├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pyre_configuration ├── LICENSE ├── README.md ├── config.json ├── conftest.py ├── prometheus_eaton_ups_exporter.py ├── prometheus_eaton_ups_exporter ├── __init__.py ├── exporter.py ├── scraper.py └── scraper_globals.py ├── pyproject.toml └── tests ├── __init__.py ├── cassettes ├── test_collect_threading.yaml ├── test_get_measures.yaml ├── test_load_rest_api.yaml ├── test_login.yaml ├── test_multi_collect.yaml └── test_single_collect.yaml ├── fixtures └── dummy_config.json ├── test_prometheus_api_exporter.py └── test_ups_api_scraper.py /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: prometheus_eaton_ups_exporter tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ['3.11'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: git config 23 | run: | 24 | git config --global user.email "eaton@exporter.com" 25 | git config --global user.name "Eaton Exporter" 26 | - name: Install dependencies 27 | run: | 28 | pip install -e ".[tests]" 29 | - name: flake8 linting 30 | run: | 31 | flake8 32 | - name: Pyre type-checking 33 | run: | 34 | pyre --noninteractive check 35 | - name: Test with pytest 36 | run: | 37 | pytest -vv 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .csv 2 | venv/* 3 | .txt 4 | build/ 5 | __pycache__ 6 | *egg-info 7 | .pyre 8 | -------------------------------------------------------------------------------- /.pyre_configuration: -------------------------------------------------------------------------------- 1 | { 2 | "site_package_search_strategy": "pep561", 3 | "source_directories": [ 4 | "." 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | 3 | Copyright (c) 2021 Mathis Lövenich 4 | 5 | Permission to use, copy, modify, and/or distribute this 6 | software for any purpose with or without fee is hereby granted, 7 | provided that the above copyright notice and this permission 8 | notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL 11 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED 12 | WARRANTIES OF MERCHANTABILITY AND FITNESS. 13 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 14 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER 15 | RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 16 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 17 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 18 | OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eaton UPS Prometheus Exporter 2 | 3 | ## Description 4 | 5 | A Prometheus exporter for Eaton UPSs. Data is collected from the REST API of the 6 | web UI of Eaton UPSs and is made available for Prometheus to scrape. 7 | 8 | The exporter can monitor multiple UPSs. 9 | 10 | ## Information Exported 11 | - Input Voltage (V) 12 | - Input Frequency (Hz) 13 | - Output Voltage (V) 14 | - Output Frequency (Hz) 15 | - Output Current (A) 16 | - Output Apparent Power (VA) 17 | - Output Active Power (W) 18 | - Output Power Factor 19 | - Output Percent Load (%) 20 | - Battery Voltage (V) 21 | - Battery Capacity (%) 22 | - Battery Remaining Time (s) 23 | - Battery Health Status (given as the remaining lifetime in years [uncertain, contribute to [#19](https://github.com/psyinfra/prometheus-eaton-ups-exporter/issues/19)]) 24 | 25 | ## Supported Devices: 26 | * Eaton 5P 1550iR ([user guide](https://www.eaton.com/content/dam/eaton/products/backup-power-ups-surge-it-power-distribution/power-management-software-connectivity/eaton-gigabit-network-card/eaton-network-m2-user-guide.pdf)) 27 | * Other models may also work if they use the same API 28 | 29 | ## Usage: 30 | UPSs to monitor and their credentials are defined in a config file. See 31 | `config.json` for an example. 32 | 33 | ``` 34 | ./prometheus_eaton_ups_exporter.py [-h] [-w WEB.LISTEN_ADDRESS] -c CONFIG [-k] [-t] [-v] [--login-timeout {range 2 - 10}] 35 | 36 | 37 | optional arguments: 38 | -h, --help show this help message and exit 39 | -w WEB.LISTEN_ADDRESS, --web.listen-address WEB.LISTEN_ADDRESS 40 | Interface and port to listen on, in the format of "ip_address:port". 41 | If the IP is omitted, the exporter listens on all interfaces. (default: 0.0.0.0:9795) 42 | -c CONFIG, --config CONFIG 43 | Configuration JSON file containing UPS addresses and login info (default: None) 44 | -k, --insecure Allow the exporter to connect to UPSs with self-signed SSL certificates (default: False) 45 | -t, --threading Whether to use multi-threading for scraping (faster) (default: False) 46 | -v, --verbose Be more verbose (default: False) 47 | --login-timeout {range 2 - 10} 48 | The login timeout for the UPSs in seconds (default: 3) 49 | 50 | ``` 51 | 52 | ## Defaults: 53 | * Default host-address is `0.0.0.0` 54 | * Default port is 9795 (see also: [Prometheus default port allocations](https://github.com/prometheus/prometheus/wiki/Default-port-allocations)) 55 | * Login timeout is set to 3 seconds 56 | * Other request timeouts are set to 2 seconds 57 | * Static values are described in `prometheus_eaton_ups_exporter/scraper_globals.py` 58 | 59 | ## Requirements: 60 | - requests 61 | - [prometheus_client](https://github.com/prometheus/client_python) 62 | 63 | # Installation: 64 | git clone https://github.com/psyinfra/prometheus-eaton-ups-exporter.git 65 | cd prometheus-eaton-ups-exporter 66 | pip install -r requirements.txt 67 | 68 | # Testing: 69 | 70 | Install requirements with 71 | 72 | pip install -r test-requirements.txt 73 | 74 | Runt tests with 75 | 76 | pytest tests 77 | 78 | #### Test-Requirements: 79 | - pytest 80 | - pytest-vcr 81 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ups1_name": { 3 | "address": "https://address.to.ups1", 4 | "user": "username", 5 | "password": "password" 6 | }, 7 | "ups2_name": { 8 | "address": "https://address.to.ups2", 9 | "user": "username", 10 | "password": "password" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope='module') 8 | def ups_scraper_conf(): 9 | """ 10 | Allows plugins and conftest files to perform initial configuration. 11 | This hook is called for every plugin and initial conftest 12 | file after command line options have been parsed. 13 | """ 14 | 15 | config_file = ( 16 | 'tests/fixtures/config.json' 17 | if os.path.exists('tests/fixtures/config.json') 18 | else 'tests/fixtures/dummy_config.json' 19 | ) 20 | 21 | try: 22 | with open(config_file) as json_file: 23 | config = json.load(json_file) 24 | except FileNotFoundError as err: 25 | print(err) 26 | 27 | return config 28 | -------------------------------------------------------------------------------- /prometheus_eaton_ups_exporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Prometheus exporter for single or multiple Eaton UPSs.""" 3 | import sys 4 | import time 5 | import traceback 6 | from typing import Tuple 7 | from argparse import ( 8 | Action, 9 | ArgumentParser, 10 | HelpFormatter, 11 | Namespace, 12 | OPTIONAL, 13 | SUPPRESS, 14 | ZERO_OR_MORE 15 | ) 16 | 17 | from prometheus_client import start_http_server, REGISTRY 18 | from prometheus_eaton_ups_exporter.scraper_globals import REQUEST_TIMEOUT 19 | from prometheus_eaton_ups_exporter.exporter import UPSMultiExporter 20 | 21 | DEFAULT_PORT = 9795 22 | DEFAULT_HOST = "0.0.0.0" 23 | 24 | 25 | class CustomFormatter(HelpFormatter): 26 | """Custom argparse formatter to provide defaults and to split newlines.""" 27 | 28 | def _split_lines(self, 29 | text: str, 30 | width: int) -> str: 31 | """Help message formatter which retains formatting of all help text.""" 32 | return text.splitlines() 33 | 34 | def _get_help_string(self, 35 | action: Action) -> str: 36 | """Help message formatter which adds default values to 37 | argument help.""" 38 | help = action.help 39 | if '%(default)' not in action.help: 40 | if action.default is not SUPPRESS: 41 | defaulting_nargs = [OPTIONAL, ZERO_OR_MORE] 42 | if action.option_strings or action.nargs in defaulting_nargs: 43 | help += ' (default: %(default)s)' 44 | return help 45 | 46 | 47 | class Range(object): 48 | 49 | def __init__(self, 50 | start: int, 51 | end: int) -> None: 52 | self.start = start 53 | self.end = end 54 | self._name_parser_map = {} 55 | 56 | def __eq__(self, 57 | other: int) -> bool: 58 | return self.start <= other <= self.end 59 | 60 | def __repr__(self) -> str: 61 | return f"range {self.start} - {self.end}" 62 | 63 | 64 | def create_parser() -> ArgumentParser: 65 | """Prepare command line arguments.""" 66 | parser = ArgumentParser( 67 | description="Prometheus Exporter for Eaton UPSs.", 68 | formatter_class=CustomFormatter 69 | ) 70 | parser.add_argument( 71 | '-w', '--web.listen-address', 72 | help='Interface and port to listen on, ' 73 | 'in the format of "ip_address:port".\n' 74 | 'If the IP is omitted, the exporter listens on all interfaces.', 75 | default=f"{DEFAULT_HOST}:{DEFAULT_PORT}" 76 | ) 77 | parser.add_argument( 78 | "-c", "--config", 79 | help="Configuration JSON file containing " 80 | "UPS addresses and login info", 81 | required=True 82 | ) 83 | parser.add_argument( 84 | '-k', '--insecure', 85 | action='store_true', 86 | help='Allow the exporter to connect to UPSs ' 87 | 'with self-signed SSL certificates', 88 | default=False 89 | ) 90 | parser.add_argument( 91 | '-t', '--threading', 92 | action='store_true', 93 | help='Whether to use multi-threading for scraping (faster)', 94 | default=False 95 | ) 96 | parser.add_argument( 97 | '-v', '--verbose', 98 | action='store_true', 99 | help='Be more verbose', 100 | default=False 101 | ) 102 | parser.add_argument( 103 | '--login-timeout', 104 | type=float, 105 | help='The login timeout for the UPSs in seconds', 106 | choices=[Range(REQUEST_TIMEOUT, 10)], 107 | default=3 108 | ) 109 | return parser 110 | 111 | 112 | def split_listen_address(listen_address: str) -> Tuple[str, int]: 113 | """Split listen address into host and port.""" 114 | if ':' in listen_address: 115 | host_address, port = listen_address.split(':') 116 | else: 117 | host_address = listen_address 118 | 119 | # if host_address or port were not specified, use default values 120 | if not host_address: 121 | host_address = DEFAULT_HOST 122 | if not port: 123 | port = DEFAULT_PORT 124 | 125 | return host_address, port 126 | 127 | 128 | def run(args: Namespace) -> None: 129 | """Execute the Prometheus Eaton UPS Exporter.""" 130 | parser = create_parser() 131 | args = parser.parse_args(args) 132 | 133 | listen_address = args.__getattribute__('web.listen_address') 134 | host_address, port = split_listen_address(listen_address) 135 | 136 | REGISTRY.register( 137 | UPSMultiExporter( 138 | args.config, 139 | insecure=args.insecure, 140 | verbose=args.verbose, 141 | threading=args.threading, 142 | login_timeout=args.login_timeout 143 | ) 144 | ) 145 | # Start up the server to expose the metrics. 146 | print(f"Starting Prometheus Eaton UPS Exporter on {host_address}:{port}") 147 | try: 148 | start_http_server(port=int(port), addr=host_address) 149 | except OSError as err: 150 | if args.verbose: 151 | print(traceback.format_exc()) 152 | else: 153 | print(err) 154 | sys.exit(1) 155 | 156 | # Run forever until an Error Event or Keyboard Interrupt 157 | try: 158 | while True: 159 | time.sleep(1) 160 | 161 | except KeyboardInterrupt: 162 | print("Prometheus Eaton UPS Exporter shut down") 163 | sys.exit(0) 164 | 165 | 166 | def main() -> None: 167 | run(sys.argv[1:]) 168 | 169 | 170 | if __name__ == "__main__": 171 | main() 172 | -------------------------------------------------------------------------------- /prometheus_eaton_ups_exporter/__init__.py: -------------------------------------------------------------------------------- 1 | """Initials of the Prometheus Eaton UPS Exporter.""" 2 | 3 | import logging 4 | 5 | # External (root level) logging level 6 | logging.basicConfig(level=logging.ERROR, format='ERROR: %(message)s') 7 | 8 | 9 | def create_logger(name: str, 10 | disabled: bool = False) -> logging.Logger: 11 | """Create logger for debug and error levels.""" 12 | logger = logging.Logger(name) 13 | logger.setLevel(logging.DEBUG) 14 | 15 | # create console handler and set level to debug 16 | debug_sh = logging.StreamHandler() 17 | debug_sh.setLevel(logging.DEBUG) 18 | 19 | # create debug formatter 20 | debug_formatter = logging.Formatter( 21 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 22 | ) 23 | # add formatter to debug_sh 24 | debug_sh.setFormatter(debug_formatter) 25 | logger.addHandler(debug_sh) 26 | 27 | # Create console handler and set level to error 28 | error_sh = logging.StreamHandler() 29 | error_sh.setLevel(logging.ERROR) 30 | 31 | # create error formatter 32 | error_formatter = logging.Formatter( 33 | 'ERROR %(name)s:%(lineno)s %(message)s' 34 | ) 35 | # add formatter to error_sh 36 | error_sh.setFormatter(error_formatter) 37 | logger.addHandler(error_sh) 38 | logger.disabled = disabled 39 | return logger 40 | -------------------------------------------------------------------------------- /prometheus_eaton_ups_exporter/exporter.py: -------------------------------------------------------------------------------- 1 | """Create and run a Prometheus Exporter for an Eaton UPS.""" 2 | import json 3 | 4 | from concurrent.futures import ThreadPoolExecutor, as_completed 5 | from concurrent.futures._base import TimeoutError 6 | from prometheus_client.core import GaugeMetricFamily 7 | 8 | from prometheus_eaton_ups_exporter import create_logger 9 | from prometheus_eaton_ups_exporter.scraper import UPSScraper 10 | 11 | from typing import Generator, Tuple 12 | 13 | NORMAL_EXECUTION = 0 14 | 15 | 16 | class UPSExporter: 17 | """Prometheus single exporter. 18 | 19 | :param ups_address: str 20 | Address to a UPS, either an IP address or a DNS hostname 21 | :param authentication: (username: str, password: str) 22 | Username and password for the web UI of the UPS 23 | :param insecure: bool 24 | Whether to connect to UPSs with self-signed SSL certificates 25 | :param verbose: bool 26 | Allow logging output for development. 27 | :param login_timeout: int 28 | Login timeout for authentication 29 | """ 30 | def __init__( 31 | self, 32 | ups_address: str, 33 | authentication: Tuple[str, str], 34 | name: str | None = None, 35 | insecure: bool = False, 36 | verbose: bool = False, 37 | login_timeout: int = 3 38 | ) -> None: 39 | self.logger = create_logger( 40 | f"{__name__}.{self.__class__.__name__}", not verbose 41 | ) 42 | self.ups_scraper = UPSScraper( 43 | ups_address, 44 | authentication, 45 | name, 46 | insecure=insecure, 47 | verbose=verbose, 48 | login_timeout=login_timeout 49 | ) 50 | 51 | def collect(self) -> Generator[GaugeMetricFamily, None, None]: 52 | """Export UPS metrics on request.""" 53 | ups_data = self.scrape_data() 54 | for measures in ups_data: 55 | if not measures: 56 | continue 57 | 58 | ups_id = measures.get('ups_id') 59 | inputs = measures.get('ups_inputs') 60 | outputs = measures.get('ups_outputs') 61 | powerbank_details = measures.get('ups_powerbank') 62 | 63 | inputs_rm = inputs['measures'] 64 | if 'realtime' in inputs['measures']: 65 | inputs_rm = inputs['measures']['realtime'] 66 | 67 | outputs_rm = outputs['measures'] 68 | if 'realtime' in outputs['measures']: 69 | outputs_rm = outputs['measures']['realtime'] 70 | 71 | powerbank_m = powerbank_details['measures'] 72 | powerbank_s = powerbank_details['status'] 73 | 74 | gauge = GaugeMetricFamily( 75 | "eaton_ups_input_volts", 76 | 'UPS input voltage (V)', 77 | labels=['ups_id'] 78 | ) 79 | gauge.add_metric([ups_id], inputs_rm['voltage']) 80 | yield gauge 81 | 82 | gauge = GaugeMetricFamily( 83 | "eaton_ups_input_hertz", 84 | 'UPS input frequency (Hz)', 85 | labels=['ups_id'] 86 | ) 87 | gauge.add_metric([ups_id], inputs_rm['frequency']) 88 | yield gauge 89 | 90 | gauge = GaugeMetricFamily( 91 | "eaton_ups_input_amperes", 92 | 'UPS input current (A)', 93 | labels=['ups_id'] 94 | ) 95 | gauge.add_metric([ups_id], inputs_rm.get('current', 0)) 96 | yield gauge 97 | 98 | gauge = GaugeMetricFamily( 99 | "eaton_ups_output_volts", 100 | 'UPS output voltage (V)', 101 | labels=['ups_id'] 102 | ) 103 | gauge.add_metric([ups_id], outputs_rm['voltage']) 104 | yield gauge 105 | 106 | gauge = GaugeMetricFamily( 107 | "eaton_ups_output_hertz", 108 | 'UPS output frequency (Hz)', 109 | labels=['ups_id'] 110 | ) 111 | gauge.add_metric([ups_id], outputs_rm['frequency']) 112 | yield gauge 113 | 114 | gauge = GaugeMetricFamily( 115 | "eaton_ups_output_amperes", 116 | 'UPS output current (A)', 117 | labels=['ups_id'] 118 | ) 119 | gauge.add_metric([ups_id], outputs_rm['current']) 120 | yield gauge 121 | 122 | gauge = GaugeMetricFamily( 123 | "eaton_ups_output_voltamperes", 124 | 'UPS output apparent power (VA)', 125 | labels=['ups_id'] 126 | ) 127 | gauge.add_metric([ups_id], outputs_rm['apparentPower']) 128 | yield gauge 129 | 130 | gauge = GaugeMetricFamily( 131 | "eaton_ups_output_watts", 132 | 'UPS output active power (W)', 133 | labels=['ups_id'] 134 | ) 135 | gauge.add_metric([ups_id], outputs_rm['activePower']) 136 | yield gauge 137 | 138 | gauge = GaugeMetricFamily( 139 | "eaton_ups_output_power_factor", 140 | 'UPS output power factor', 141 | labels=['ups_id'] 142 | ) 143 | gauge.add_metric([ups_id], outputs_rm['powerFactor']) 144 | yield gauge 145 | 146 | gauge = GaugeMetricFamily( 147 | "eaton_ups_output_load_ratio", 148 | "Ratio of the output apparent power vs. " 149 | "the UPS's capacity in VA.", 150 | labels=['ups_id'] 151 | ) 152 | gauge.add_metric([ups_id], int(outputs_rm['percentLoad']) / 100) 153 | yield gauge 154 | 155 | gauge = GaugeMetricFamily( 156 | "eaton_ups_battery_volts", 157 | 'UPS battery voltage (V)', 158 | labels=['ups_id'] 159 | ) 160 | gauge.add_metric([ups_id], powerbank_m['voltage']) 161 | yield gauge 162 | 163 | gauge = GaugeMetricFamily( 164 | "eaton_ups_battery_capacity_ratio", 165 | 'Ratio of the remaining charge vs the total battery capacity', 166 | labels=['ups_id'] 167 | ) 168 | gauge.add_metric( 169 | [ups_id], 170 | int(powerbank_m.get('remainingChargeCapacity', 0)) / 100 171 | ) 172 | yield gauge 173 | 174 | gauge = GaugeMetricFamily( 175 | "eaton_ups_battery_remaining_seconds", 176 | 'UPS remaining battery time (s)', 177 | labels=['ups_id'] 178 | ) 179 | gauge.add_metric([ups_id], powerbank_m['remainingTime']) 180 | yield gauge 181 | 182 | gauge = GaugeMetricFamily( 183 | "eaton_ups_battery_health", 184 | 'UPS health status given as the ' 185 | 'remaining lifetime (years) [uncertain]', 186 | labels=['ups_id'] 187 | ) 188 | health_val = powerbank_s['health'] 189 | health = 0 190 | if isinstance(health_val, int): 191 | health = health_val 192 | gauge.add_metric([ups_id], health) 193 | yield gauge 194 | 195 | def scrape_data(self): 196 | """Scrape measure data. 197 | 198 | :return: measures 199 | """ 200 | yield self.ups_scraper.get_measures() 201 | 202 | 203 | class UPSMultiExporter(UPSExporter): 204 | """Prometheus exporter for multiple UPSs. 205 | 206 | Collects metrics from multiple UPSs at the same time. If threading is 207 | enabled, multiple threads will be used to collect sensor readings which is 208 | considerably faster. 209 | 210 | :param config: str 211 | Path to the configuration file, containing UPS ip/hostname, username, 212 | and password combinations for all UPSs to be monitored 213 | :param insecure: bool 214 | Whether to connect to UPSs with self-signed SSL certificates 215 | :param threading: bool 216 | Whether to use multiple threads to scrape the data 'parallel'. 217 | This is surely the best way to increase the speed 218 | :param verbose: bool 219 | Allow logging output for development 220 | :param login_timeout: int 221 | Login timeout for authentication 222 | """ 223 | 224 | def __init__( 225 | self, 226 | config: str, 227 | insecure: bool = False, 228 | threading: bool = False, 229 | verbose: bool = False, 230 | login_timeout: int = 3 231 | ) -> None: 232 | self.logger = create_logger( 233 | f"{__name__}.{self.__class__.__name__}", not verbose 234 | ) 235 | self.insecure = insecure 236 | self.threading = threading 237 | self.verbose = verbose 238 | self.login_timeout = login_timeout 239 | self.ups_devices = self.get_ups_devices(config) 240 | 241 | @staticmethod 242 | def get_devices(config: str | dict) -> dict: 243 | """Take a config file path or config dict of UPSs.""" 244 | if isinstance(config, str): 245 | with open(config) as json_file: 246 | devices = json.load(json_file) 247 | elif isinstance(config, dict): 248 | devices = config 249 | else: 250 | raise AttributeError("Only config path (str) or dict accepted") 251 | return devices 252 | 253 | def get_ups_devices(self, 254 | config: str | dict) -> list: 255 | """Creates multiple UPSScraper. 256 | 257 | :param config: str | dict 258 | Path to a JSON-based config file or a config dict 259 | :return: list 260 | List of UPSScrapers 261 | """ 262 | devices = self.get_devices(config) 263 | 264 | return [ 265 | UPSScraper( 266 | value['address'], 267 | (value['user'], value['password']), 268 | key, 269 | insecure=self.insecure, 270 | verbose=self.verbose, 271 | login_timeout=self.login_timeout 272 | ) 273 | for key, value in devices.items() 274 | ] 275 | 276 | def scrape_data(self): 277 | """Scrape measure data. 278 | 279 | :return: measures 280 | """ 281 | if self.threading: 282 | with ThreadPoolExecutor() as executor: 283 | futures = [ 284 | executor.submit(ups.get_measures) 285 | for ups in self.ups_devices 286 | ] 287 | try: 288 | for future in as_completed(futures, self.login_timeout+1): 289 | yield future.result() 290 | except TimeoutError as err: 291 | self.logger.exception(err) 292 | yield None 293 | 294 | else: 295 | for ups in self.ups_devices: 296 | yield ups.get_measures() 297 | -------------------------------------------------------------------------------- /prometheus_eaton_ups_exporter/scraper.py: -------------------------------------------------------------------------------- 1 | """REST API web scraper for Eaton UPS measure data.""" 2 | import json 3 | 4 | from requests import Session, Response 5 | from requests.exceptions import ( 6 | ConnectionError, 7 | InvalidURL, 8 | MissingSchema, 9 | ReadTimeout, 10 | SSLError, 11 | ) 12 | # pyre-ignore[21]: pyre thinks urllib3 is not part of requests 13 | from requests.packages import urllib3 14 | from prometheus_eaton_ups_exporter import create_logger 15 | from prometheus_eaton_ups_exporter.scraper_globals import ( 16 | AUTHENTICATION_FAILED, 17 | CERTIFICATE_VERIFY_FAILED, 18 | CONNECTION_ERROR, 19 | INPUT_MEMBER_ID, 20 | INVALID_URL_ERROR, 21 | LOGIN_AUTH_PATH, 22 | LOGIN_DATA, 23 | LoginFailedException, 24 | MISSING_SCHEMA_ERROR, 25 | OUTPUT_MEMBER_ID, 26 | REQUEST_TIMEOUT, 27 | REST_API_PATH, 28 | SSL_ERROR, 29 | TIMEOUT_ERROR, 30 | ) 31 | from typing import Tuple 32 | 33 | 34 | class UPSScraper: 35 | """ 36 | Create a UPS Scraper based on the Eaton UPS's API. 37 | 38 | :param ups_address: str 39 | Address to a UPS, either an IP address or a DNS hostname 40 | :param authentication: (username: str, password: str) 41 | Username and password for the web UI of the UPS 42 | :param name: str 43 | Name of the UPS. 44 | Used as identifier to differentiate between multiple UPSs. 45 | :param insecure: bool 46 | Whether to connect to UPSs with self-signed SSL certificates 47 | :param verbose: bool 48 | Allow logging output for development 49 | :param login_timeout: float 50 | Login timeout for authentication 51 | """ 52 | def __init__(self, 53 | ups_address: str, 54 | authentication: Tuple[str, str], 55 | name: str | None = None, 56 | insecure: bool = False, 57 | verbose: bool = False, 58 | login_timeout: int = 3) -> None: 59 | self.ups_address = ups_address 60 | self.username, self.password = authentication 61 | self.name = name 62 | self.login_timeout = login_timeout 63 | self.session = Session() 64 | self.logger = create_logger(__name__, not verbose) 65 | 66 | # ignore self signed certificate 67 | self.session.verify = not insecure 68 | # disable warnings created because of ignoring certificates 69 | if not self.session.verify: 70 | # pyre-ignore[16]: pyre thinks urllib3 is not part of requests 71 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 72 | 73 | self.token_type, self.access_token = None, None 74 | 75 | def login(self) -> Tuple[str, str]: 76 | """ 77 | Login to the UPS Web UI. 78 | 79 | Based on analysing the UPS Web UI, this will create a POST request 80 | with the authentication details to successfully create a session 81 | on the specified UPS. 82 | 83 | :return: two for the authentication necessary string values 84 | """ 85 | try: 86 | data = LOGIN_DATA 87 | data["username"] = self.username 88 | data["password"] = self.password 89 | 90 | login_request = self.session.post( 91 | self.ups_address + LOGIN_AUTH_PATH, 92 | data=json.dumps(data), # needs to be JSON encoded 93 | timeout=self.login_timeout 94 | ) 95 | login_response = login_request.json() 96 | 97 | token_type = login_response['token_type'] 98 | access_token = login_response['access_token'] 99 | 100 | self.logger.debug( 101 | "Authentication successful on (%s)", 102 | self.ups_address 103 | ) 104 | 105 | return token_type, access_token 106 | except KeyError: 107 | raise LoginFailedException( 108 | AUTHENTICATION_FAILED, 109 | "Authentication failed" 110 | ) from None 111 | except SSLError as err: 112 | if 'CERTIFICATE_VERIFY_FAILED' in str(err): 113 | # print("Try -k to allow insecure server " 114 | # "connections when using SSL") 115 | raise LoginFailedException( 116 | CERTIFICATE_VERIFY_FAILED, 117 | "Invalid certificate, connection to host failed" 118 | ) from None 119 | raise LoginFailedException( 120 | SSL_ERROR, 121 | "Connection refused due to an SSL Error" 122 | ) from None 123 | except ConnectionError: 124 | raise LoginFailedException( 125 | CONNECTION_ERROR, 126 | "Connection refused, host might be out of reach." 127 | ) from None 128 | except ReadTimeout: 129 | raise LoginFailedException( 130 | TIMEOUT_ERROR, 131 | f"Login Timeout > {self.login_timeout} seconds" 132 | ) from None 133 | except MissingSchema: 134 | raise LoginFailedException( 135 | MISSING_SCHEMA_ERROR, 136 | "Invalid URL, no schema supplied" 137 | ) from None 138 | except InvalidURL: 139 | raise LoginFailedException( 140 | INVALID_URL_ERROR, 141 | "Invalid URL, no host supplied" 142 | ) from None 143 | 144 | def load_page(self, 145 | url: bytes | str) -> Response: 146 | """ 147 | Load a webpage of the UPS Web UI or API. 148 | 149 | This will try to load the page by the given URL. 150 | If authentication is needed first, the login function gets executed 151 | before loading the specified page. 152 | 153 | :param url: ups web url 154 | :return: request.Response 155 | """ 156 | headers = { 157 | "Connection": "keep-alive", 158 | "Authorization": f"{self.token_type} {self.access_token}", 159 | } 160 | 161 | try: 162 | request = self.session.get( 163 | url, 164 | headers=headers, 165 | timeout=REQUEST_TIMEOUT 166 | ) 167 | 168 | # Session might be expired, connect again 169 | try: 170 | if "errorCode" in request.json() or "code" in request.json(): 171 | self.logger.debug('Session expired, reconnect') 172 | self.token_type, self.access_token = self.login() 173 | return self.load_page(url) 174 | except ValueError: 175 | pass 176 | 177 | # try to login, if not authorized 178 | if "Unauthorized" in request.text: 179 | self.logger.debug('Unauthorized, try to login') 180 | try: 181 | self.token_type, self.access_token = self.login() 182 | return self.load_page(url) 183 | except LoginFailedException as err: 184 | if err.error_code == TIMEOUT_ERROR: 185 | raise LoginFailedException( 186 | AUTHENTICATION_FAILED, 187 | "Authentication failed" 188 | ) from err 189 | # else 190 | raise err 191 | 192 | self.logger.debug('GET %s', url) 193 | return request 194 | except ConnectionError: 195 | self.logger.debug('Connection Error try to login again') 196 | try: 197 | self.token_type, self.access_token = self.login() 198 | return self.load_page(url) 199 | except LoginFailedException: 200 | raise 201 | except LoginFailedException: 202 | raise 203 | 204 | def get_measures(self) -> dict: 205 | """ 206 | Get most relevant UPS metrics. 207 | 208 | :return: { 209 | "ups_id": self.name, 210 | "ups_inputs": inputs, 211 | "ups_outputs": outputs, 212 | "ups_powerbank": powerbank 213 | } 214 | """ 215 | measurements = dict() 216 | try: 217 | power_dist_request = self.load_page( 218 | self.ups_address+REST_API_PATH 219 | ) 220 | power_dist_overview = power_dist_request.json() 221 | 222 | if not self.name: 223 | self.name = f"ups_{power_dist_overview['id']}" 224 | 225 | ups_inputs_api = power_dist_overview['inputs']['@id'] 226 | ups_ouptups_api = power_dist_overview['outputs']['@id'] 227 | 228 | inputs_request = self.load_page( 229 | self.ups_address + ups_inputs_api + f'/{INPUT_MEMBER_ID}' 230 | ) 231 | inputs = inputs_request.json() 232 | 233 | outputs_request = self.load_page( 234 | self.ups_address + ups_ouptups_api + f'/{OUTPUT_MEMBER_ID}' 235 | ) 236 | outputs = outputs_request.json() 237 | 238 | ups_backup_sys_api = power_dist_overview['backupSystem']['@id'] 239 | backup_request = self.load_page( 240 | self.ups_address + ups_backup_sys_api 241 | ) 242 | backup = backup_request.json() 243 | ups_powerbank_api = backup['powerBank']['@id'] 244 | powerbank_request = self.load_page( 245 | self.ups_address + ups_powerbank_api 246 | ) 247 | powerbank = powerbank_request.json() 248 | 249 | measurements = { 250 | "ups_id": self.name, 251 | "ups_inputs": inputs, 252 | "ups_outputs": outputs, 253 | "ups_powerbank": powerbank 254 | } 255 | 256 | except LoginFailedException as err: 257 | self.logger.error(err) 258 | print(f"{err.__class__.__name__} - ({self.ups_address}): " 259 | f"{err.message}") 260 | except json.decoder.JSONDecodeError as err: 261 | self.logger.debug("This needs to be solved by a developer") 262 | self.logger.error(err) 263 | except Exception: 264 | raise 265 | 266 | return measurements 267 | -------------------------------------------------------------------------------- /prometheus_eaton_ups_exporter/scraper_globals.py: -------------------------------------------------------------------------------- 1 | """ 2 | Hardcoded globals used by the scraper. 3 | 4 | This will need new configurations when the API changes. 5 | """ 6 | 7 | # these will be added to the original url (ex: https://eaton.ups.com) 8 | LOGIN_AUTH_PATH = '/rest/mbdetnrs/1.0/oauth2/token' 9 | REST_API_PATH = '/rest/mbdetnrs/1.0/powerDistributions/1' 10 | 11 | # As there could be multiple inputs and outputs, 12 | # take the first one 13 | INPUT_MEMBER_ID = 1 14 | OUTPUT_MEMBER_ID = 1 15 | 16 | # Data to post to the login form. 17 | # Must be extended by username and password. 18 | LOGIN_DATA = { 19 | "grant_type": "password", 20 | "scope": "GUIAccess" 21 | } 22 | 23 | # Timeouts in seconds 24 | REQUEST_TIMEOUT = 2 25 | 26 | # Exit Codes 27 | NORMAL_EXECUTION = 0 28 | AUTHENTICATION_FAILED = 1 29 | SSL_ERROR = 2 30 | CERTIFICATE_VERIFY_FAILED = 3 31 | CONNECTION_ERROR = 4 32 | TIMEOUT_ERROR = 5 33 | MISSING_SCHEMA_ERROR = 6 34 | INVALID_URL_ERROR = 7 35 | 36 | 37 | class LoginFailedException(Exception): 38 | """Exception raised for failed login. 39 | 40 | :param error_code: Error code 41 | :param message: Error description message 42 | """ 43 | 44 | def __init__(self, 45 | error_code: int, 46 | message: str) -> None: 47 | self.error_code = error_code 48 | self.message = message 49 | super().__init__(self.error_code, self.message) 50 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "prometheus-eaton-ups-exporter" 7 | authors = [ 8 | {name = "Mathis Lövenich"}, 9 | ] 10 | description = "A Prometheus exporter for Eaton UPSs." 11 | version = "1.2.0" 12 | readme = "README.md" 13 | requires-python = ">=3.11" 14 | keywords = ["prometheus", "exporter", "eaton"] 15 | license = {text = "ISC"} 16 | classifiers = [ 17 | "Programming Language :: Python :: 3", 18 | ] 19 | dependencies = [ 20 | 'prometheus_client', 21 | 'requests', 22 | ] 23 | 24 | [project.urls] 25 | "Bug Reports" = "https://github.com/psyinfra/prometheus-eaton-ups-exporter/issues" 26 | "Source" = "https://github.com/psyinfra/prometheus-eaton-ups-exporter/" 27 | 28 | [project.optional-dependencies] 29 | tests = [ 30 | 'flake8', 31 | 'pyre-check', 32 | 'pytest == 7.2.1', 33 | 'pytest-vcr', 34 | ] 35 | 36 | [project.scripts] 37 | prometheus_eaton_ups_exporter = "prometheus_eaton_ups_exporter.main:main" 38 | 39 | [tool.setuptools.packages] 40 | find = {} # Scanning implicit namespaces is active by default 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def first_ups_details(conf): 5 | ups_name = list(conf.keys())[0] 6 | address = conf[ups_name]['address'] 7 | auth = ( 8 | conf[ups_name]['user'], 9 | conf[ups_name]['password'] 10 | ) 11 | return address, auth, ups_name 12 | 13 | 14 | def scrub_body(): 15 | def before_record_request(request): 16 | try: 17 | body_json = json.loads( 18 | request.body.decode("utf-8").replace("'", '"') 19 | ) 20 | body_json['username'] = 'username' 21 | body_json['password'] = 'password' 22 | request.body = str(body_json).replace("'", '"').encode('utf-8') 23 | return request 24 | except AttributeError: 25 | return request 26 | return before_record_request 27 | -------------------------------------------------------------------------------- /tests/cassettes/test_collect_threading.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - None None 11 | Connection: 12 | - keep-alive 13 | User-Agent: 14 | - python-requests/2.31.0 15 | method: GET 16 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 17 | response: 18 | body: 19 | string: '
Unauthorized
20 | 21 | ' 22 | headers: 23 | Connection: 24 | - Keep-Alive 25 | Content-Length: 26 | - '60' 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Wed, 24 Apr 2024 09:34:43 GMT 31 | Keep-Alive: 32 | - timeout=30s, max=999 33 | Server: 34 | - Tntnet 35 | status: 36 | code: 401 37 | message: Unauthorized 38 | - request: 39 | body: null 40 | headers: 41 | Accept: 42 | - '*/*' 43 | Accept-Encoding: 44 | - gzip, deflate 45 | Authorization: 46 | - Bearer YWI2MGJlNmYzZTYwMzg3ZWIy 47 | Connection: 48 | - keep-alive 49 | User-Agent: 50 | - python-requests/2.31.0 51 | method: GET 52 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1 53 | response: 54 | body: 55 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 56 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"4e0f9ee6-a743-5d21-bea6-a5dd7b59b8a1\",\n\t\t\"vendor\": 57 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K45017\",\n\t\t\"type\": 58 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 59 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 60 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 61 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 62 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 63 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 64 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 65 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 66 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 67 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 68 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 69 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 70 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 71 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 72 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 73 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 74 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/-42qpTLgWgi2aZTwNh9MGw\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 75 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/L2q_IUCRVoSzmLVeLSIK4g\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 76 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/gY16mQp7ViKuQqgI7WlA9g\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 77 | headers: 78 | Cache-Control: 79 | - no-store 80 | Connection: 81 | - Keep-Alive 82 | Content-Encoding: 83 | - gzip 84 | Content-Length: 85 | - '579' 86 | Content-Security-Policy: 87 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 88 | 'self' 89 | Content-Type: 90 | - application/json;charset=UTF-8 91 | Date: 92 | - Wed, 24 Apr 2024 09:34:44 GMT 93 | Keep-Alive: 94 | - timeout=30s, max=999 95 | Pragma: 96 | - no-cache 97 | Server: 98 | - Tntnet 99 | Strict-Transport-Security: 100 | - max-age=31536000 101 | X-Content-Type-Options: 102 | - nosniff 103 | X-XSS-Protection: 104 | - '1' 105 | status: 106 | code: 200 107 | message: OK 108 | - request: 109 | body: null 110 | headers: 111 | Accept: 112 | - '*/*' 113 | Accept-Encoding: 114 | - gzip, deflate 115 | Authorization: 116 | - Bearer OTZlYmE3MTlkMjkzNDM5Y2Y0 117 | Connection: 118 | - keep-alive 119 | User-Agent: 120 | - python-requests/2.31.0 121 | method: GET 122 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 123 | response: 124 | body: 125 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 126 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"1e3e39e3-d95d-5113-b897-bd31290f82fb\",\n\t\t\"vendor\": 127 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K46202\",\n\t\t\"type\": 128 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 129 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 130 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 131 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 132 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 133 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 134 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 135 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 136 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 137 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 138 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 139 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 140 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 141 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 142 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 143 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 144 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/_SHkPSSPUVm3Uvme8XaZ2w\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 145 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/ntyTH9M9VHCqR4ONgALeUA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 146 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/KZDUq7PyUfuj5OpvnZr7Iw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 147 | headers: 148 | Cache-Control: 149 | - no-store 150 | Connection: 151 | - Keep-Alive 152 | Content-Encoding: 153 | - gzip 154 | Content-Length: 155 | - '582' 156 | Content-Security-Policy: 157 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 158 | 'self' 159 | Content-Type: 160 | - application/json;charset=UTF-8 161 | Date: 162 | - Wed, 24 Apr 2024 09:34:45 GMT 163 | Keep-Alive: 164 | - timeout=30s, max=999 165 | Pragma: 166 | - no-cache 167 | Server: 168 | - Tntnet 169 | Strict-Transport-Security: 170 | - max-age=31536000 171 | X-Content-Type-Options: 172 | - nosniff 173 | X-XSS-Protection: 174 | - '1' 175 | status: 176 | code: 200 177 | message: OK 178 | - request: 179 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 180 | "password": password}' 181 | headers: 182 | Accept: 183 | - '*/*' 184 | Accept-Encoding: 185 | - gzip, deflate 186 | Connection: 187 | - keep-alive 188 | Content-Length: 189 | - '110' 190 | User-Agent: 191 | - python-requests/2.31.0 192 | method: POST 193 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/oauth2/token 194 | response: 195 | body: 196 | string: '{"expires_in": 898,"token_type": "Bearer","access_token": "OWQ4YzMzMjlmYWI4MDIwZjY4","user_profile": 197 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 198 | 199 | 200 | ' 201 | headers: 202 | Cache-Control: 203 | - no-store 204 | Connection: 205 | - Keep-Alive 206 | Content-Length: 207 | - '309' 208 | Content-Security-Policy: 209 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 210 | 'self' 211 | Content-Type: 212 | - application/json;charset=UTF-8 213 | Date: 214 | - Wed, 24 Apr 2024 09:34:45 GMT 215 | Keep-Alive: 216 | - timeout=30s, max=999 217 | Pragma: 218 | - no-cache 219 | Server: 220 | - Tntnet 221 | Strict-Transport-Security: 222 | - max-age=31536000 223 | X-Content-Type-Options: 224 | - nosniff 225 | X-XSS-Protection: 226 | - '1' 227 | status: 228 | code: 200 229 | message: OK 230 | - request: 231 | body: null 232 | headers: 233 | Accept: 234 | - '*/*' 235 | Accept-Encoding: 236 | - gzip, deflate 237 | Authorization: 238 | - Bearer YWI2MGJlNmYzZTYwMzg3ZWIy 239 | Connection: 240 | - keep-alive 241 | User-Agent: 242 | - python-requests/2.31.0 243 | method: GET 244 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 245 | response: 246 | body: 247 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 248 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 50,\n\t\t\t\"voltage\": 249 | 230.1,\n\t\t\t\"current\": 0,\n\t\t\t\"activePower\": 0,\n\t\t\t\"apparentPower\": 250 | 0,\n\t\t\t\"powerFactor\": 0,\n\t\t\t\"percentLoad\": 0\n\t\t}\n\t},\n\t\"status\": 251 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 252 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 253 | headers: 254 | Cache-Control: 255 | - no-store 256 | Connection: 257 | - Keep-Alive 258 | Content-Length: 259 | - '407' 260 | Content-Security-Policy: 261 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 262 | 'self' 263 | Content-Type: 264 | - application/json;charset=UTF-8 265 | Date: 266 | - Wed, 24 Apr 2024 09:34:45 GMT 267 | Keep-Alive: 268 | - timeout=30s, max=999 269 | Pragma: 270 | - no-cache 271 | Server: 272 | - Tntnet 273 | Strict-Transport-Security: 274 | - max-age=31536000 275 | X-Content-Type-Options: 276 | - nosniff 277 | X-XSS-Protection: 278 | - '1' 279 | status: 280 | code: 200 281 | message: OK 282 | - request: 283 | body: null 284 | headers: 285 | Accept: 286 | - '*/*' 287 | Accept-Encoding: 288 | - gzip, deflate 289 | Authorization: 290 | - Bearer YWI2MGJlNmYzZTYwMzg3ZWIy 291 | Connection: 292 | - keep-alive 293 | User-Agent: 294 | - python-requests/2.31.0 295 | method: GET 296 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 297 | response: 298 | body: 299 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 300 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 301 | headers: 302 | Cache-Control: 303 | - no-store 304 | Connection: 305 | - Keep-Alive 306 | Content-Length: 307 | - '161' 308 | Content-Security-Policy: 309 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 310 | 'self' 311 | Content-Type: 312 | - application/json;charset=UTF-8 313 | Date: 314 | - Wed, 24 Apr 2024 09:34:45 GMT 315 | Keep-Alive: 316 | - timeout=30s, max=999 317 | Pragma: 318 | - no-cache 319 | Server: 320 | - Tntnet 321 | Strict-Transport-Security: 322 | - max-age=31536000 323 | X-Content-Type-Options: 324 | - nosniff 325 | X-XSS-Protection: 326 | - '1' 327 | status: 328 | code: 200 329 | message: OK 330 | - request: 331 | body: null 332 | headers: 333 | Accept: 334 | - '*/*' 335 | Accept-Encoding: 336 | - gzip, deflate 337 | Authorization: 338 | - Bearer YWI2MGJlNmYzZTYwMzg3ZWIy 339 | Connection: 340 | - keep-alive 341 | User-Agent: 342 | - python-requests/2.31.0 343 | method: GET 344 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 345 | response: 346 | body: 347 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 348 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 349 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 350 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 351 | 38.9,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 15284\n\t},\n\t\"status\": 352 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 10,\n\t\t\"state\": 3,\n\t\t\"present\": 353 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 354 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 355 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712480537,\n\t\t\t\"value\": 356 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1709268280,\n\t\t\"health\": 357 | 15,\n\t\t\"expired\": true\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 358 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/1oCpTRPFVq28wuvUx1EkFA\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 359 | headers: 360 | Cache-Control: 361 | - no-store 362 | Connection: 363 | - Keep-Alive 364 | Content-Length: 365 | - '972' 366 | Content-Security-Policy: 367 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 368 | 'self' 369 | Content-Type: 370 | - application/json;charset=UTF-8 371 | Date: 372 | - Wed, 24 Apr 2024 09:34:46 GMT 373 | Keep-Alive: 374 | - timeout=30s, max=999 375 | Pragma: 376 | - no-cache 377 | Server: 378 | - Tntnet 379 | Strict-Transport-Security: 380 | - max-age=31536000 381 | X-Content-Type-Options: 382 | - nosniff 383 | X-XSS-Protection: 384 | - '1' 385 | status: 386 | code: 200 387 | message: OK 388 | version: 1 389 | -------------------------------------------------------------------------------- /tests/cassettes/test_get_measures.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - None None 11 | Connection: 12 | - keep-alive 13 | User-Agent: 14 | - python-requests/2.31.0 15 | method: GET 16 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 17 | response: 18 | body: 19 | string: 'Unauthorized
20 | 21 | ' 22 | headers: 23 | Connection: 24 | - Keep-Alive 25 | Content-Length: 26 | - '60' 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Wed, 24 Apr 2024 09:34:49 GMT 31 | Keep-Alive: 32 | - timeout=30s, max=999 33 | Server: 34 | - Tntnet 35 | status: 36 | code: 401 37 | message: Unauthorized 38 | - request: 39 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 40 | "password": password}' 41 | headers: 42 | Accept: 43 | - '*/*' 44 | Accept-Encoding: 45 | - gzip, deflate 46 | Connection: 47 | - keep-alive 48 | Content-Length: 49 | - '110' 50 | User-Agent: 51 | - python-requests/2.31.0 52 | method: POST 53 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 54 | response: 55 | body: 56 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "YTg2MzkwZGU1MTkyNWVmM2Zi","user_profile": 57 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 58 | 59 | 60 | ' 61 | headers: 62 | Cache-Control: 63 | - no-store 64 | Connection: 65 | - Keep-Alive 66 | Content-Length: 67 | - '309' 68 | Content-Security-Policy: 69 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 70 | 'self' 71 | Content-Type: 72 | - application/json;charset=UTF-8 73 | Date: 74 | - Wed, 24 Apr 2024 09:34:51 GMT 75 | Keep-Alive: 76 | - timeout=30s, max=999 77 | Pragma: 78 | - no-cache 79 | Server: 80 | - Tntnet 81 | Strict-Transport-Security: 82 | - max-age=31536000 83 | X-Content-Type-Options: 84 | - nosniff 85 | X-XSS-Protection: 86 | - '1' 87 | status: 88 | code: 200 89 | message: OK 90 | - request: 91 | body: null 92 | headers: 93 | Accept: 94 | - '*/*' 95 | Accept-Encoding: 96 | - gzip, deflate 97 | Authorization: 98 | - Bearer YTg2MzkwZGU1MTkyNWVmM2Zi 99 | Connection: 100 | - keep-alive 101 | User-Agent: 102 | - python-requests/2.31.0 103 | method: GET 104 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 105 | response: 106 | body: 107 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 108 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"1e3e39e3-d95d-5113-b897-bd31290f82fb\",\n\t\t\"vendor\": 109 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K46202\",\n\t\t\"type\": 110 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 111 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 112 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 113 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 114 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 115 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 116 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 117 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 118 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 119 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 120 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 121 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 122 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 123 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 124 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 125 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 126 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/_SHkPSSPUVm3Uvme8XaZ2w\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 127 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/ntyTH9M9VHCqR4ONgALeUA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 128 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/KZDUq7PyUfuj5OpvnZr7Iw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 129 | headers: 130 | Cache-Control: 131 | - no-store 132 | Connection: 133 | - Keep-Alive 134 | Content-Encoding: 135 | - gzip 136 | Content-Length: 137 | - '582' 138 | Content-Security-Policy: 139 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 140 | 'self' 141 | Content-Type: 142 | - application/json;charset=UTF-8 143 | Date: 144 | - Wed, 24 Apr 2024 09:34:51 GMT 145 | Keep-Alive: 146 | - timeout=30s, max=999 147 | Pragma: 148 | - no-cache 149 | Server: 150 | - Tntnet 151 | Strict-Transport-Security: 152 | - max-age=31536000 153 | X-Content-Type-Options: 154 | - nosniff 155 | X-XSS-Protection: 156 | - '1' 157 | status: 158 | code: 200 159 | message: OK 160 | - request: 161 | body: null 162 | headers: 163 | Accept: 164 | - '*/*' 165 | Accept-Encoding: 166 | - gzip, deflate 167 | Authorization: 168 | - Bearer YTg2MzkwZGU1MTkyNWVmM2Zi 169 | Connection: 170 | - keep-alive 171 | User-Agent: 172 | - python-requests/2.31.0 173 | method: GET 174 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1 175 | response: 176 | body: 177 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1\",\n\t\"id\": 178 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 50,\n\t\t\t\"voltage\": 179 | 228.2,\n\t\t\t\"current\": 0.1\n\t\t}\n\t},\n\t\"status\": {\n\t\t\"operating\": 180 | 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": {\n\t\t\"@id\": 181 | \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1/phases\"\n\t}\n}\n" 182 | headers: 183 | Cache-Control: 184 | - no-store 185 | Connection: 186 | - Keep-Alive 187 | Content-Length: 188 | - '321' 189 | Content-Security-Policy: 190 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 191 | 'self' 192 | Content-Type: 193 | - application/json;charset=UTF-8 194 | Date: 195 | - Wed, 24 Apr 2024 09:34:52 GMT 196 | Keep-Alive: 197 | - timeout=30s, max=999 198 | Pragma: 199 | - no-cache 200 | Server: 201 | - Tntnet 202 | Strict-Transport-Security: 203 | - max-age=31536000 204 | X-Content-Type-Options: 205 | - nosniff 206 | X-XSS-Protection: 207 | - '1' 208 | status: 209 | code: 200 210 | message: OK 211 | - request: 212 | body: null 213 | headers: 214 | Accept: 215 | - '*/*' 216 | Accept-Encoding: 217 | - gzip, deflate 218 | Authorization: 219 | - Bearer YTg2MzkwZGU1MTkyNWVmM2Zi 220 | Connection: 221 | - keep-alive 222 | User-Agent: 223 | - python-requests/2.31.0 224 | method: GET 225 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 226 | response: 227 | body: 228 | string: "{\"classCode\":1,\n \"errorCode\":204\n}\n" 229 | headers: 230 | Connection: 231 | - Keep-Alive 232 | Content-Length: 233 | - '35' 234 | Content-Type: 235 | - text/html; charset=UTF-8 236 | Date: 237 | - Wed, 24 Apr 2024 09:34:52 GMT 238 | Keep-Alive: 239 | - timeout=30s, max=999 240 | Server: 241 | - Tntnet 242 | WWW-Authenticate: 243 | - Bearer realm="genepy" 244 | status: 245 | code: 401 246 | message: '' 247 | - request: 248 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 249 | "password": password}' 250 | headers: 251 | Accept: 252 | - '*/*' 253 | Accept-Encoding: 254 | - gzip, deflate 255 | Connection: 256 | - keep-alive 257 | Content-Length: 258 | - '110' 259 | User-Agent: 260 | - python-requests/2.31.0 261 | method: POST 262 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 263 | response: 264 | body: 265 | string: '{"expires_in": 898,"token_type": "Bearer","access_token": "NGZhZWQ4ODE5MzY3OTU1MmNk","user_profile": 266 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 267 | 268 | 269 | ' 270 | headers: 271 | Cache-Control: 272 | - no-store 273 | Connection: 274 | - Keep-Alive 275 | Content-Length: 276 | - '309' 277 | Content-Security-Policy: 278 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 279 | 'self' 280 | Content-Type: 281 | - application/json;charset=UTF-8 282 | Date: 283 | - Wed, 24 Apr 2024 09:34:55 GMT 284 | Keep-Alive: 285 | - timeout=30s, max=999 286 | Pragma: 287 | - no-cache 288 | Server: 289 | - Tntnet 290 | Strict-Transport-Security: 291 | - max-age=31536000 292 | X-Content-Type-Options: 293 | - nosniff 294 | X-XSS-Protection: 295 | - '1' 296 | status: 297 | code: 200 298 | message: OK 299 | - request: 300 | body: null 301 | headers: 302 | Accept: 303 | - '*/*' 304 | Accept-Encoding: 305 | - gzip, deflate 306 | Authorization: 307 | - Bearer NGZhZWQ4ODE5MzY3OTU1MmNk 308 | Connection: 309 | - keep-alive 310 | User-Agent: 311 | - python-requests/2.31.0 312 | method: GET 313 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 314 | response: 315 | body: 316 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 317 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 50,\n\t\t\t\"voltage\": 318 | 228.2,\n\t\t\t\"current\": 0,\n\t\t\t\"activePower\": 0,\n\t\t\t\"apparentPower\": 319 | 0,\n\t\t\t\"powerFactor\": 0,\n\t\t\t\"percentLoad\": 0\n\t\t}\n\t},\n\t\"status\": 320 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 321 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 322 | headers: 323 | Cache-Control: 324 | - no-store 325 | Connection: 326 | - Keep-Alive 327 | Content-Length: 328 | - '407' 329 | Content-Security-Policy: 330 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 331 | 'self' 332 | Content-Type: 333 | - application/json;charset=UTF-8 334 | Date: 335 | - Wed, 24 Apr 2024 09:34:55 GMT 336 | Keep-Alive: 337 | - timeout=30s, max=999 338 | Pragma: 339 | - no-cache 340 | Server: 341 | - Tntnet 342 | Strict-Transport-Security: 343 | - max-age=31536000 344 | X-Content-Type-Options: 345 | - nosniff 346 | X-XSS-Protection: 347 | - '1' 348 | status: 349 | code: 200 350 | message: OK 351 | - request: 352 | body: null 353 | headers: 354 | Accept: 355 | - '*/*' 356 | Accept-Encoding: 357 | - gzip, deflate 358 | Authorization: 359 | - Bearer NGZhZWQ4ODE5MzY3OTU1MmNk 360 | Connection: 361 | - keep-alive 362 | User-Agent: 363 | - python-requests/2.31.0 364 | method: GET 365 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 366 | response: 367 | body: 368 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 369 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 370 | headers: 371 | Cache-Control: 372 | - no-store 373 | Connection: 374 | - Keep-Alive 375 | Content-Length: 376 | - '161' 377 | Content-Security-Policy: 378 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 379 | 'self' 380 | Content-Type: 381 | - application/json;charset=UTF-8 382 | Date: 383 | - Wed, 24 Apr 2024 09:34:55 GMT 384 | Keep-Alive: 385 | - timeout=30s, max=999 386 | Pragma: 387 | - no-cache 388 | Server: 389 | - Tntnet 390 | Strict-Transport-Security: 391 | - max-age=31536000 392 | X-Content-Type-Options: 393 | - nosniff 394 | X-XSS-Protection: 395 | - '1' 396 | status: 397 | code: 200 398 | message: OK 399 | - request: 400 | body: null 401 | headers: 402 | Accept: 403 | - '*/*' 404 | Accept-Encoding: 405 | - gzip, deflate 406 | Authorization: 407 | - Bearer NGZhZWQ4ODE5MzY3OTU1MmNk 408 | Connection: 409 | - keep-alive 410 | User-Agent: 411 | - python-requests/2.31.0 412 | method: GET 413 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 414 | response: 415 | body: 416 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 417 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 418 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 419 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 420 | 38.8,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 15284\n\t},\n\t\"status\": 421 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 5,\n\t\t\"state\": 1,\n\t\t\"present\": 422 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 423 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 424 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712304878,\n\t\t\t\"value\": 425 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1709270908,\n\t\t\"health\": 426 | 15,\n\t\t\"expired\": true\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 427 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/S2VQRv4BWEKo-lgmRsRDVQ\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 428 | headers: 429 | Cache-Control: 430 | - no-store 431 | Connection: 432 | - Keep-Alive 433 | Content-Length: 434 | - '971' 435 | Content-Security-Policy: 436 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 437 | 'self' 438 | Content-Type: 439 | - application/json;charset=UTF-8 440 | Date: 441 | - Wed, 24 Apr 2024 09:34:56 GMT 442 | Keep-Alive: 443 | - timeout=30s, max=999 444 | Pragma: 445 | - no-cache 446 | Server: 447 | - Tntnet 448 | Strict-Transport-Security: 449 | - max-age=31536000 450 | X-Content-Type-Options: 451 | - nosniff 452 | X-XSS-Protection: 453 | - '1' 454 | status: 455 | code: 200 456 | message: OK 457 | version: 1 458 | -------------------------------------------------------------------------------- /tests/cassettes/test_load_rest_api.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - None None 11 | Connection: 12 | - keep-alive 13 | User-Agent: 14 | - python-requests/2.31.0 15 | method: GET 16 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 17 | response: 18 | body: 19 | string: 'Unauthorized
20 | 21 | ' 22 | headers: 23 | Connection: 24 | - Keep-Alive 25 | Content-Length: 26 | - '60' 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Wed, 24 Apr 2024 09:34:47 GMT 31 | Keep-Alive: 32 | - timeout=30s, max=999 33 | Server: 34 | - Tntnet 35 | status: 36 | code: 401 37 | message: Unauthorized 38 | - request: 39 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 40 | "password": password}' 41 | headers: 42 | Accept: 43 | - '*/*' 44 | Accept-Encoding: 45 | - gzip, deflate 46 | Connection: 47 | - keep-alive 48 | Content-Length: 49 | - '110' 50 | User-Agent: 51 | - python-requests/2.31.0 52 | method: POST 53 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 54 | response: 55 | body: 56 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "MTRhNzJmNjliMTkzMzEyZDMz","user_profile": 57 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 58 | 59 | 60 | ' 61 | headers: 62 | Cache-Control: 63 | - no-store 64 | Connection: 65 | - Keep-Alive 66 | Content-Length: 67 | - '309' 68 | Content-Security-Policy: 69 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 70 | 'self' 71 | Content-Type: 72 | - application/json;charset=UTF-8 73 | Date: 74 | - Wed, 24 Apr 2024 09:34:49 GMT 75 | Keep-Alive: 76 | - timeout=30s, max=999 77 | Pragma: 78 | - no-cache 79 | Server: 80 | - Tntnet 81 | Strict-Transport-Security: 82 | - max-age=31536000 83 | X-Content-Type-Options: 84 | - nosniff 85 | X-XSS-Protection: 86 | - '1' 87 | status: 88 | code: 200 89 | message: OK 90 | - request: 91 | body: null 92 | headers: 93 | Accept: 94 | - '*/*' 95 | Accept-Encoding: 96 | - gzip, deflate 97 | Authorization: 98 | - Bearer MTRhNzJmNjliMTkzMzEyZDMz 99 | Connection: 100 | - keep-alive 101 | User-Agent: 102 | - python-requests/2.31.0 103 | method: GET 104 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 105 | response: 106 | body: 107 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 108 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"1e3e39e3-d95d-5113-b897-bd31290f82fb\",\n\t\t\"vendor\": 109 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K46202\",\n\t\t\"type\": 110 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 111 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 112 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 113 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 114 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 115 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 116 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 117 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 118 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 119 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 120 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 121 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 122 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 123 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 124 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 125 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 126 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/_SHkPSSPUVm3Uvme8XaZ2w\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 127 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/ntyTH9M9VHCqR4ONgALeUA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 128 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/KZDUq7PyUfuj5OpvnZr7Iw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 129 | headers: 130 | Cache-Control: 131 | - no-store 132 | Connection: 133 | - Keep-Alive 134 | Content-Encoding: 135 | - gzip 136 | Content-Length: 137 | - '582' 138 | Content-Security-Policy: 139 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 140 | 'self' 141 | Content-Type: 142 | - application/json;charset=UTF-8 143 | Date: 144 | - Wed, 24 Apr 2024 09:34:49 GMT 145 | Keep-Alive: 146 | - timeout=30s, max=999 147 | Pragma: 148 | - no-cache 149 | Server: 150 | - Tntnet 151 | Strict-Transport-Security: 152 | - max-age=31536000 153 | X-Content-Type-Options: 154 | - nosniff 155 | X-XSS-Protection: 156 | - '1' 157 | status: 158 | code: 200 159 | message: OK 160 | version: 1 161 | -------------------------------------------------------------------------------- /tests/cassettes/test_login.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 4 | "password": password}' 5 | headers: 6 | Accept: 7 | - '*/*' 8 | Accept-Encoding: 9 | - gzip, deflate 10 | Connection: 11 | - keep-alive 12 | Content-Length: 13 | - '110' 14 | User-Agent: 15 | - python-requests/2.31.0 16 | method: POST 17 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 18 | response: 19 | body: 20 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "ODAwNjZkZmNhZWFjNTNjYzc2","user_profile": 21 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 22 | 23 | 24 | ' 25 | headers: 26 | Cache-Control: 27 | - no-store 28 | Connection: 29 | - Keep-Alive 30 | Content-Length: 31 | - '309' 32 | Content-Security-Policy: 33 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 34 | 'self' 35 | Content-Type: 36 | - application/json;charset=UTF-8 37 | Date: 38 | - Wed, 24 Apr 2024 09:34:47 GMT 39 | Keep-Alive: 40 | - timeout=30s, max=999 41 | Pragma: 42 | - no-cache 43 | Server: 44 | - Tntnet 45 | Strict-Transport-Security: 46 | - max-age=31536000 47 | X-Content-Type-Options: 48 | - nosniff 49 | X-XSS-Protection: 50 | - '1' 51 | status: 52 | code: 200 53 | message: OK 54 | version: 1 55 | -------------------------------------------------------------------------------- /tests/cassettes/test_multi_collect.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - None None 11 | Connection: 12 | - keep-alive 13 | User-Agent: 14 | - python-requests/2.31.0 15 | method: GET 16 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 17 | response: 18 | body: 19 | string: 'Unauthorized
20 | 21 | ' 22 | headers: 23 | Connection: 24 | - Keep-Alive 25 | Content-Length: 26 | - '60' 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Wed, 24 Apr 2024 09:34:33 GMT 31 | Keep-Alive: 32 | - timeout=30s, max=999 33 | Server: 34 | - Tntnet 35 | status: 36 | code: 401 37 | message: Unauthorized 38 | - request: 39 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 40 | "password": password}' 41 | headers: 42 | Accept: 43 | - '*/*' 44 | Accept-Encoding: 45 | - gzip, deflate 46 | Connection: 47 | - keep-alive 48 | Content-Length: 49 | - '110' 50 | User-Agent: 51 | - python-requests/2.31.0 52 | method: POST 53 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 54 | response: 55 | body: 56 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "MjE2ZmI4OWI3ZWZlYzRlODU0","user_profile": 57 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 58 | 59 | 60 | ' 61 | headers: 62 | Cache-Control: 63 | - no-store 64 | Connection: 65 | - Keep-Alive 66 | Content-Length: 67 | - '309' 68 | Content-Security-Policy: 69 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 70 | 'self' 71 | Content-Type: 72 | - application/json;charset=UTF-8 73 | Date: 74 | - Wed, 24 Apr 2024 09:34:34 GMT 75 | Keep-Alive: 76 | - timeout=30s, max=999 77 | Pragma: 78 | - no-cache 79 | Server: 80 | - Tntnet 81 | Strict-Transport-Security: 82 | - max-age=31536000 83 | X-Content-Type-Options: 84 | - nosniff 85 | X-XSS-Protection: 86 | - '1' 87 | status: 88 | code: 200 89 | message: OK 90 | - request: 91 | body: null 92 | headers: 93 | Accept: 94 | - '*/*' 95 | Accept-Encoding: 96 | - gzip, deflate 97 | Authorization: 98 | - Bearer MjE2ZmI4OWI3ZWZlYzRlODU0 99 | Connection: 100 | - keep-alive 101 | User-Agent: 102 | - python-requests/2.31.0 103 | method: GET 104 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 105 | response: 106 | body: 107 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 108 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"1e3e39e3-d95d-5113-b897-bd31290f82fb\",\n\t\t\"vendor\": 109 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K46202\",\n\t\t\"type\": 110 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 111 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 112 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 113 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 114 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 115 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 116 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 117 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 118 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 119 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 120 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 121 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 122 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 123 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 124 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 125 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 126 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/_SHkPSSPUVm3Uvme8XaZ2w\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 127 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/ntyTH9M9VHCqR4ONgALeUA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 128 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/KZDUq7PyUfuj5OpvnZr7Iw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 129 | headers: 130 | Cache-Control: 131 | - no-store 132 | Connection: 133 | - Keep-Alive 134 | Content-Encoding: 135 | - gzip 136 | Content-Length: 137 | - '582' 138 | Content-Security-Policy: 139 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 140 | 'self' 141 | Content-Type: 142 | - application/json;charset=UTF-8 143 | Date: 144 | - Wed, 24 Apr 2024 09:34:34 GMT 145 | Keep-Alive: 146 | - timeout=30s, max=999 147 | Pragma: 148 | - no-cache 149 | Server: 150 | - Tntnet 151 | Strict-Transport-Security: 152 | - max-age=31536000 153 | X-Content-Type-Options: 154 | - nosniff 155 | X-XSS-Protection: 156 | - '1' 157 | status: 158 | code: 200 159 | message: OK 160 | - request: 161 | body: null 162 | headers: 163 | Accept: 164 | - '*/*' 165 | Accept-Encoding: 166 | - gzip, deflate 167 | Authorization: 168 | - Bearer MjE2ZmI4OWI3ZWZlYzRlODU0 169 | Connection: 170 | - keep-alive 171 | User-Agent: 172 | - python-requests/2.31.0 173 | method: GET 174 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1 175 | response: 176 | body: 177 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1\",\n\t\"id\": 178 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 179 | 227.6,\n\t\t\t\"current\": 0.1\n\t\t}\n\t},\n\t\"status\": {\n\t\t\"operating\": 180 | 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": {\n\t\t\"@id\": 181 | \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1/phases\"\n\t}\n}\n" 182 | headers: 183 | Cache-Control: 184 | - no-store 185 | Connection: 186 | - Keep-Alive 187 | Content-Length: 188 | - '323' 189 | Content-Security-Policy: 190 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 191 | 'self' 192 | Content-Type: 193 | - application/json;charset=UTF-8 194 | Date: 195 | - Wed, 24 Apr 2024 09:34:34 GMT 196 | Keep-Alive: 197 | - timeout=30s, max=999 198 | Pragma: 199 | - no-cache 200 | Server: 201 | - Tntnet 202 | Strict-Transport-Security: 203 | - max-age=31536000 204 | X-Content-Type-Options: 205 | - nosniff 206 | X-XSS-Protection: 207 | - '1' 208 | status: 209 | code: 200 210 | message: OK 211 | - request: 212 | body: null 213 | headers: 214 | Accept: 215 | - '*/*' 216 | Accept-Encoding: 217 | - gzip, deflate 218 | Authorization: 219 | - Bearer MjE2ZmI4OWI3ZWZlYzRlODU0 220 | Connection: 221 | - keep-alive 222 | User-Agent: 223 | - python-requests/2.31.0 224 | method: GET 225 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 226 | response: 227 | body: 228 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 229 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 230 | 228.5,\n\t\t\t\"current\": 0,\n\t\t\t\"activePower\": 0,\n\t\t\t\"apparentPower\": 231 | 0,\n\t\t\t\"powerFactor\": 0,\n\t\t\t\"percentLoad\": 0\n\t\t}\n\t},\n\t\"status\": 232 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 233 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 234 | headers: 235 | Cache-Control: 236 | - no-store 237 | Connection: 238 | - Keep-Alive 239 | Content-Length: 240 | - '409' 241 | Content-Security-Policy: 242 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 243 | 'self' 244 | Content-Type: 245 | - application/json;charset=UTF-8 246 | Date: 247 | - Wed, 24 Apr 2024 09:34:35 GMT 248 | Keep-Alive: 249 | - timeout=30s, max=999 250 | Pragma: 251 | - no-cache 252 | Server: 253 | - Tntnet 254 | Strict-Transport-Security: 255 | - max-age=31536000 256 | X-Content-Type-Options: 257 | - nosniff 258 | X-XSS-Protection: 259 | - '1' 260 | status: 261 | code: 200 262 | message: OK 263 | - request: 264 | body: null 265 | headers: 266 | Accept: 267 | - '*/*' 268 | Accept-Encoding: 269 | - gzip, deflate 270 | Authorization: 271 | - Bearer MjE2ZmI4OWI3ZWZlYzRlODU0 272 | Connection: 273 | - keep-alive 274 | User-Agent: 275 | - python-requests/2.31.0 276 | method: GET 277 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 278 | response: 279 | body: 280 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 281 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 282 | headers: 283 | Cache-Control: 284 | - no-store 285 | Connection: 286 | - Keep-Alive 287 | Content-Length: 288 | - '161' 289 | Content-Security-Policy: 290 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 291 | 'self' 292 | Content-Type: 293 | - application/json;charset=UTF-8 294 | Date: 295 | - Wed, 24 Apr 2024 09:34:35 GMT 296 | Keep-Alive: 297 | - timeout=30s, max=999 298 | Pragma: 299 | - no-cache 300 | Server: 301 | - Tntnet 302 | Strict-Transport-Security: 303 | - max-age=31536000 304 | X-Content-Type-Options: 305 | - nosniff 306 | X-XSS-Protection: 307 | - '1' 308 | status: 309 | code: 200 310 | message: OK 311 | - request: 312 | body: null 313 | headers: 314 | Accept: 315 | - '*/*' 316 | Accept-Encoding: 317 | - gzip, deflate 318 | Authorization: 319 | - Bearer MjE2ZmI4OWI3ZWZlYzRlODU0 320 | Connection: 321 | - keep-alive 322 | User-Agent: 323 | - python-requests/2.31.0 324 | method: GET 325 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 326 | response: 327 | body: 328 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 329 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 330 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 331 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 332 | 38.8,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 15284\n\t},\n\t\"status\": 333 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 5,\n\t\t\"state\": 1,\n\t\t\"present\": 334 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 335 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 336 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712304878,\n\t\t\t\"value\": 337 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1709270908,\n\t\t\"health\": 338 | 15,\n\t\t\"expired\": true\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 339 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/S2VQRv4BWEKo-lgmRsRDVQ\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 340 | headers: 341 | Cache-Control: 342 | - no-store 343 | Connection: 344 | - Keep-Alive 345 | Content-Length: 346 | - '971' 347 | Content-Security-Policy: 348 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 349 | 'self' 350 | Content-Type: 351 | - application/json;charset=UTF-8 352 | Date: 353 | - Wed, 24 Apr 2024 09:34:36 GMT 354 | Keep-Alive: 355 | - timeout=30s, max=999 356 | Pragma: 357 | - no-cache 358 | Server: 359 | - Tntnet 360 | Strict-Transport-Security: 361 | - max-age=31536000 362 | X-Content-Type-Options: 363 | - nosniff 364 | X-XSS-Protection: 365 | - '1' 366 | status: 367 | code: 200 368 | message: OK 369 | - request: 370 | body: null 371 | headers: 372 | Accept: 373 | - '*/*' 374 | Accept-Encoding: 375 | - gzip, deflate 376 | Authorization: 377 | - None None 378 | Connection: 379 | - keep-alive 380 | User-Agent: 381 | - python-requests/2.31.0 382 | method: GET 383 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1 384 | response: 385 | body: 386 | string: 'Unauthorized
387 | 388 | ' 389 | headers: 390 | Connection: 391 | - Keep-Alive 392 | Content-Length: 393 | - '60' 394 | Content-Type: 395 | - text/html; charset=UTF-8 396 | Date: 397 | - Wed, 24 Apr 2024 09:34:36 GMT 398 | Keep-Alive: 399 | - timeout=30s, max=999 400 | Server: 401 | - Tntnet 402 | status: 403 | code: 401 404 | message: Unauthorized 405 | - request: 406 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 407 | "password": password}' 408 | headers: 409 | Accept: 410 | - '*/*' 411 | Accept-Encoding: 412 | - gzip, deflate 413 | Connection: 414 | - keep-alive 415 | Content-Length: 416 | - '110' 417 | User-Agent: 418 | - python-requests/2.31.0 419 | method: POST 420 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/oauth2/token 421 | response: 422 | body: 423 | string: '{"expires_in": 898,"token_type": "Bearer","access_token": "MmMzNTBlM2NiNjBlY2EzOGE5","user_profile": 424 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 425 | 426 | 427 | ' 428 | headers: 429 | Cache-Control: 430 | - no-store 431 | Connection: 432 | - Keep-Alive 433 | Content-Length: 434 | - '309' 435 | Content-Security-Policy: 436 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 437 | 'self' 438 | Content-Type: 439 | - application/json;charset=UTF-8 440 | Date: 441 | - Wed, 24 Apr 2024 09:34:38 GMT 442 | Keep-Alive: 443 | - timeout=30s, max=999 444 | Pragma: 445 | - no-cache 446 | Server: 447 | - Tntnet 448 | Strict-Transport-Security: 449 | - max-age=31536000 450 | X-Content-Type-Options: 451 | - nosniff 452 | X-XSS-Protection: 453 | - '1' 454 | status: 455 | code: 200 456 | message: OK 457 | - request: 458 | body: null 459 | headers: 460 | Accept: 461 | - '*/*' 462 | Accept-Encoding: 463 | - gzip, deflate 464 | Authorization: 465 | - Bearer MmMzNTBlM2NiNjBlY2EzOGE5 466 | Connection: 467 | - keep-alive 468 | User-Agent: 469 | - python-requests/2.31.0 470 | method: GET 471 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1 472 | response: 473 | body: 474 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 475 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"4e0f9ee6-a743-5d21-bea6-a5dd7b59b8a1\",\n\t\t\"vendor\": 476 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K45017\",\n\t\t\"type\": 477 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 478 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 479 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 480 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 481 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 482 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 483 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 484 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 485 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 486 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 487 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 488 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 489 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 490 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 491 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 492 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 493 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/-42qpTLgWgi2aZTwNh9MGw\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 494 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/L2q_IUCRVoSzmLVeLSIK4g\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 495 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/gY16mQp7ViKuQqgI7WlA9g\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 496 | headers: 497 | Cache-Control: 498 | - no-store 499 | Connection: 500 | - Keep-Alive 501 | Content-Encoding: 502 | - gzip 503 | Content-Length: 504 | - '579' 505 | Content-Security-Policy: 506 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 507 | 'self' 508 | Content-Type: 509 | - application/json;charset=UTF-8 510 | Date: 511 | - Wed, 24 Apr 2024 09:34:38 GMT 512 | Keep-Alive: 513 | - timeout=30s, max=999 514 | Pragma: 515 | - no-cache 516 | Server: 517 | - Tntnet 518 | Strict-Transport-Security: 519 | - max-age=31536000 520 | X-Content-Type-Options: 521 | - nosniff 522 | X-XSS-Protection: 523 | - '1' 524 | status: 525 | code: 200 526 | message: OK 527 | - request: 528 | body: null 529 | headers: 530 | Accept: 531 | - '*/*' 532 | Accept-Encoding: 533 | - gzip, deflate 534 | Authorization: 535 | - Bearer MmMzNTBlM2NiNjBlY2EzOGE5 536 | Connection: 537 | - keep-alive 538 | User-Agent: 539 | - python-requests/2.31.0 540 | method: GET 541 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1 542 | response: 543 | body: 544 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1\",\n\t\"id\": 545 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 546 | 230.1,\n\t\t\t\"current\": 0.1\n\t\t}\n\t},\n\t\"status\": {\n\t\t\"operating\": 547 | 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": {\n\t\t\"@id\": 548 | \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1/phases\"\n\t}\n}\n" 549 | headers: 550 | Cache-Control: 551 | - no-store 552 | Connection: 553 | - Keep-Alive 554 | Content-Length: 555 | - '323' 556 | Content-Security-Policy: 557 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 558 | 'self' 559 | Content-Type: 560 | - application/json;charset=UTF-8 561 | Date: 562 | - Wed, 24 Apr 2024 09:34:38 GMT 563 | Keep-Alive: 564 | - timeout=30s, max=999 565 | Pragma: 566 | - no-cache 567 | Server: 568 | - Tntnet 569 | Strict-Transport-Security: 570 | - max-age=31536000 571 | X-Content-Type-Options: 572 | - nosniff 573 | X-XSS-Protection: 574 | - '1' 575 | status: 576 | code: 200 577 | message: OK 578 | - request: 579 | body: null 580 | headers: 581 | Accept: 582 | - '*/*' 583 | Accept-Encoding: 584 | - gzip, deflate 585 | Authorization: 586 | - Bearer MmMzNTBlM2NiNjBlY2EzOGE5 587 | Connection: 588 | - keep-alive 589 | User-Agent: 590 | - python-requests/2.31.0 591 | method: GET 592 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 593 | response: 594 | body: 595 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 596 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 597 | 230.1,\n\t\t\t\"current\": 0,\n\t\t\t\"activePower\": 0,\n\t\t\t\"apparentPower\": 598 | 0,\n\t\t\t\"powerFactor\": 0,\n\t\t\t\"percentLoad\": 0\n\t\t}\n\t},\n\t\"status\": 599 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 600 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 601 | headers: 602 | Cache-Control: 603 | - no-store 604 | Connection: 605 | - Keep-Alive 606 | Content-Length: 607 | - '409' 608 | Content-Security-Policy: 609 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 610 | 'self' 611 | Content-Type: 612 | - application/json;charset=UTF-8 613 | Date: 614 | - Wed, 24 Apr 2024 09:34:39 GMT 615 | Keep-Alive: 616 | - timeout=30s, max=999 617 | Pragma: 618 | - no-cache 619 | Server: 620 | - Tntnet 621 | Strict-Transport-Security: 622 | - max-age=31536000 623 | X-Content-Type-Options: 624 | - nosniff 625 | X-XSS-Protection: 626 | - '1' 627 | status: 628 | code: 200 629 | message: OK 630 | - request: 631 | body: null 632 | headers: 633 | Accept: 634 | - '*/*' 635 | Accept-Encoding: 636 | - gzip, deflate 637 | Authorization: 638 | - Bearer MmMzNTBlM2NiNjBlY2EzOGE5 639 | Connection: 640 | - keep-alive 641 | User-Agent: 642 | - python-requests/2.31.0 643 | method: GET 644 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 645 | response: 646 | body: 647 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 648 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 649 | headers: 650 | Cache-Control: 651 | - no-store 652 | Connection: 653 | - Keep-Alive 654 | Content-Length: 655 | - '161' 656 | Content-Security-Policy: 657 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 658 | 'self' 659 | Content-Type: 660 | - application/json;charset=UTF-8 661 | Date: 662 | - Wed, 24 Apr 2024 09:34:39 GMT 663 | Keep-Alive: 664 | - timeout=30s, max=999 665 | Pragma: 666 | - no-cache 667 | Server: 668 | - Tntnet 669 | Strict-Transport-Security: 670 | - max-age=31536000 671 | X-Content-Type-Options: 672 | - nosniff 673 | X-XSS-Protection: 674 | - '1' 675 | status: 676 | code: 200 677 | message: OK 678 | - request: 679 | body: null 680 | headers: 681 | Accept: 682 | - '*/*' 683 | Accept-Encoding: 684 | - gzip, deflate 685 | Authorization: 686 | - Bearer MmMzNTBlM2NiNjBlY2EzOGE5 687 | Connection: 688 | - keep-alive 689 | User-Agent: 690 | - python-requests/2.31.0 691 | method: GET 692 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 693 | response: 694 | body: 695 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 696 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 697 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 698 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 699 | 38.9,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 15284\n\t},\n\t\"status\": 700 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 10,\n\t\t\"state\": 3,\n\t\t\"present\": 701 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 702 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 703 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712480537,\n\t\t\t\"value\": 704 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1709268280,\n\t\t\"health\": 705 | 15,\n\t\t\"expired\": true\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 706 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/1oCpTRPFVq28wuvUx1EkFA\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 707 | headers: 708 | Cache-Control: 709 | - no-store 710 | Connection: 711 | - Keep-Alive 712 | Content-Length: 713 | - '972' 714 | Content-Security-Policy: 715 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 716 | 'self' 717 | Content-Type: 718 | - application/json;charset=UTF-8 719 | Date: 720 | - Wed, 24 Apr 2024 09:34:39 GMT 721 | Keep-Alive: 722 | - timeout=30s, max=999 723 | Pragma: 724 | - no-cache 725 | Server: 726 | - Tntnet 727 | Strict-Transport-Security: 728 | - max-age=31536000 729 | X-Content-Type-Options: 730 | - nosniff 731 | X-XSS-Protection: 732 | - '1' 733 | status: 734 | code: 200 735 | message: OK 736 | - request: 737 | body: null 738 | headers: 739 | Accept: 740 | - '*/*' 741 | Accept-Encoding: 742 | - gzip, deflate 743 | Authorization: 744 | - None None 745 | Connection: 746 | - keep-alive 747 | User-Agent: 748 | - python-requests/2.31.0 749 | method: GET 750 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1 751 | response: 752 | body: 753 | string: 'Unauthorized
754 | 755 | ' 756 | headers: 757 | Connection: 758 | - Keep-Alive 759 | Content-Length: 760 | - '60' 761 | Content-Type: 762 | - text/html; charset=UTF-8 763 | Date: 764 | - Wed, 24 Apr 2024 09:34:40 GMT 765 | Keep-Alive: 766 | - timeout=30s, max=999 767 | Server: 768 | - Tntnet 769 | status: 770 | code: 401 771 | message: Unauthorized 772 | - request: 773 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 774 | "password": password}' 775 | headers: 776 | Accept: 777 | - '*/*' 778 | Accept-Encoding: 779 | - gzip, deflate 780 | Connection: 781 | - keep-alive 782 | Content-Length: 783 | - '110' 784 | User-Agent: 785 | - python-requests/2.31.0 786 | method: POST 787 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/oauth2/token 788 | response: 789 | body: 790 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "ZDdhNTkzMzYwMzhiOWFhNmYy","user_profile": 791 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 792 | 793 | 794 | ' 795 | headers: 796 | Cache-Control: 797 | - no-store 798 | Connection: 799 | - Keep-Alive 800 | Content-Length: 801 | - '309' 802 | Content-Security-Policy: 803 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 804 | 'self' 805 | Content-Type: 806 | - application/json;charset=UTF-8 807 | Date: 808 | - Wed, 24 Apr 2024 09:34:41 GMT 809 | Keep-Alive: 810 | - timeout=30s, max=999 811 | Pragma: 812 | - no-cache 813 | Server: 814 | - Tntnet 815 | Strict-Transport-Security: 816 | - max-age=31536000 817 | X-Content-Type-Options: 818 | - nosniff 819 | X-XSS-Protection: 820 | - '1' 821 | status: 822 | code: 200 823 | message: OK 824 | - request: 825 | body: null 826 | headers: 827 | Accept: 828 | - '*/*' 829 | Accept-Encoding: 830 | - gzip, deflate 831 | Authorization: 832 | - Bearer ZDdhNTkzMzYwMzhiOWFhNmYy 833 | Connection: 834 | - keep-alive 835 | User-Agent: 836 | - python-requests/2.31.0 837 | method: GET 838 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1 839 | response: 840 | body: 841 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 842 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"94d25b82-d428-57c1-b71f-9e9ea2cbc1b6\",\n\t\t\"vendor\": 843 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117L35116\",\n\t\t\"type\": 844 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 845 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 846 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 847 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 848 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 849 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 850 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 851 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 852 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 853 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 854 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 855 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 856 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 857 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 858 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 859 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 860 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/rVluig55X7Kw52UKhwI4hg\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 861 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/fGRVkUZdWDKue4BTLhhQFA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 862 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/HoKlYN3RX4qoLCd_B6bDFg\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 863 | headers: 864 | Cache-Control: 865 | - no-store 866 | Connection: 867 | - Keep-Alive 868 | Content-Encoding: 869 | - gzip 870 | Content-Length: 871 | - '581' 872 | Content-Security-Policy: 873 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 874 | 'self' 875 | Content-Type: 876 | - application/json;charset=UTF-8 877 | Date: 878 | - Wed, 24 Apr 2024 09:34:41 GMT 879 | Keep-Alive: 880 | - timeout=30s, max=999 881 | Pragma: 882 | - no-cache 883 | Server: 884 | - Tntnet 885 | Strict-Transport-Security: 886 | - max-age=31536000 887 | X-Content-Type-Options: 888 | - nosniff 889 | X-XSS-Protection: 890 | - '1' 891 | status: 892 | code: 200 893 | message: OK 894 | - request: 895 | body: null 896 | headers: 897 | Accept: 898 | - '*/*' 899 | Accept-Encoding: 900 | - gzip, deflate 901 | Authorization: 902 | - Bearer ZDdhNTkzMzYwMzhiOWFhNmYy 903 | Connection: 904 | - keep-alive 905 | User-Agent: 906 | - python-requests/2.31.0 907 | method: GET 908 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1 909 | response: 910 | body: 911 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1\",\n\t\"id\": 912 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 913 | 229.2,\n\t\t\t\"current\": 0.8\n\t\t}\n\t},\n\t\"status\": {\n\t\t\"operating\": 914 | 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": {\n\t\t\"@id\": 915 | \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1/phases\"\n\t}\n}\n" 916 | headers: 917 | Cache-Control: 918 | - no-store 919 | Connection: 920 | - Keep-Alive 921 | Content-Length: 922 | - '323' 923 | Content-Security-Policy: 924 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 925 | 'self' 926 | Content-Type: 927 | - application/json;charset=UTF-8 928 | Date: 929 | - Wed, 24 Apr 2024 09:34:42 GMT 930 | Keep-Alive: 931 | - timeout=30s, max=999 932 | Pragma: 933 | - no-cache 934 | Server: 935 | - Tntnet 936 | Strict-Transport-Security: 937 | - max-age=31536000 938 | X-Content-Type-Options: 939 | - nosniff 940 | X-XSS-Protection: 941 | - '1' 942 | status: 943 | code: 200 944 | message: OK 945 | - request: 946 | body: null 947 | headers: 948 | Accept: 949 | - '*/*' 950 | Accept-Encoding: 951 | - gzip, deflate 952 | Authorization: 953 | - Bearer ZDdhNTkzMzYwMzhiOWFhNmYy 954 | Connection: 955 | - keep-alive 956 | User-Agent: 957 | - python-requests/2.31.0 958 | method: GET 959 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 960 | response: 961 | body: 962 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 963 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 964 | 229.2,\n\t\t\t\"current\": 0.7,\n\t\t\t\"activePower\": 132,\n\t\t\t\"apparentPower\": 965 | 160,\n\t\t\t\"powerFactor\": 82,\n\t\t\t\"percentLoad\": 12\n\t\t}\n\t},\n\t\"status\": 966 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 967 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 968 | headers: 969 | Cache-Control: 970 | - no-store 971 | Connection: 972 | - Keep-Alive 973 | Content-Length: 974 | - '417' 975 | Content-Security-Policy: 976 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 977 | 'self' 978 | Content-Type: 979 | - application/json;charset=UTF-8 980 | Date: 981 | - Wed, 24 Apr 2024 09:34:42 GMT 982 | Keep-Alive: 983 | - timeout=30s, max=999 984 | Pragma: 985 | - no-cache 986 | Server: 987 | - Tntnet 988 | Strict-Transport-Security: 989 | - max-age=31536000 990 | X-Content-Type-Options: 991 | - nosniff 992 | X-XSS-Protection: 993 | - '1' 994 | status: 995 | code: 200 996 | message: OK 997 | - request: 998 | body: null 999 | headers: 1000 | Accept: 1001 | - '*/*' 1002 | Accept-Encoding: 1003 | - gzip, deflate 1004 | Authorization: 1005 | - Bearer ZDdhNTkzMzYwMzhiOWFhNmYy 1006 | Connection: 1007 | - keep-alive 1008 | User-Agent: 1009 | - python-requests/2.31.0 1010 | method: GET 1011 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 1012 | response: 1013 | body: 1014 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 1015 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 1016 | headers: 1017 | Cache-Control: 1018 | - no-store 1019 | Connection: 1020 | - Keep-Alive 1021 | Content-Length: 1022 | - '161' 1023 | Content-Security-Policy: 1024 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 1025 | 'self' 1026 | Content-Type: 1027 | - application/json;charset=UTF-8 1028 | Date: 1029 | - Wed, 24 Apr 2024 09:34:42 GMT 1030 | Keep-Alive: 1031 | - timeout=30s, max=999 1032 | Pragma: 1033 | - no-cache 1034 | Server: 1035 | - Tntnet 1036 | Strict-Transport-Security: 1037 | - max-age=31536000 1038 | X-Content-Type-Options: 1039 | - nosniff 1040 | X-XSS-Protection: 1041 | - '1' 1042 | status: 1043 | code: 200 1044 | message: OK 1045 | - request: 1046 | body: null 1047 | headers: 1048 | Accept: 1049 | - '*/*' 1050 | Accept-Encoding: 1051 | - gzip, deflate 1052 | Authorization: 1053 | - Bearer ZDdhNTkzMzYwMzhiOWFhNmYy 1054 | Connection: 1055 | - keep-alive 1056 | User-Agent: 1057 | - python-requests/2.31.0 1058 | method: GET 1059 | uri: https://address.to.ups2/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 1060 | response: 1061 | body: 1062 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 1063 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 1064 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 1065 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 1066 | 38.8,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 3588\n\t},\n\t\"status\": 1067 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 5,\n\t\t\"state\": 1,\n\t\t\"present\": 1068 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 1069 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 1070 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712305671,\n\t\t\t\"value\": 1071 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1739890778,\n\t\t\"health\": 1072 | 5,\n\t\t\"expired\": false\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 1073 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/jd0cQu5tXmuvphvbQ2z3Mw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 1074 | headers: 1075 | Cache-Control: 1076 | - no-store 1077 | Connection: 1078 | - Keep-Alive 1079 | Content-Length: 1080 | - '970' 1081 | Content-Security-Policy: 1082 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 1083 | 'self' 1084 | Content-Type: 1085 | - application/json;charset=UTF-8 1086 | Date: 1087 | - Wed, 24 Apr 2024 09:34:43 GMT 1088 | Keep-Alive: 1089 | - timeout=30s, max=999 1090 | Pragma: 1091 | - no-cache 1092 | Server: 1093 | - Tntnet 1094 | Strict-Transport-Security: 1095 | - max-age=31536000 1096 | X-Content-Type-Options: 1097 | - nosniff 1098 | X-XSS-Protection: 1099 | - '1' 1100 | status: 1101 | code: 200 1102 | message: OK 1103 | version: 1 1104 | -------------------------------------------------------------------------------- /tests/cassettes/test_single_collect.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: 6 | - '*/*' 7 | Accept-Encoding: 8 | - gzip, deflate 9 | Authorization: 10 | - None None 11 | Connection: 12 | - keep-alive 13 | User-Agent: 14 | - python-requests/2.31.0 15 | method: GET 16 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 17 | response: 18 | body: 19 | string: 'Unauthorized
20 | 21 | ' 22 | headers: 23 | Connection: 24 | - Keep-Alive 25 | Content-Length: 26 | - '60' 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Date: 30 | - Wed, 24 Apr 2024 09:34:29 GMT 31 | Keep-Alive: 32 | - timeout=30s, max=999 33 | Server: 34 | - Tntnet 35 | status: 36 | code: 401 37 | message: Unauthorized 38 | - request: 39 | body: '{"grant_type": "password", "scope": "GUIAccess", "username": "username", 40 | "password": password}' 41 | headers: 42 | Accept: 43 | - '*/*' 44 | Accept-Encoding: 45 | - gzip, deflate 46 | Connection: 47 | - keep-alive 48 | Content-Length: 49 | - '110' 50 | User-Agent: 51 | - python-requests/2.31.0 52 | method: POST 53 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/oauth2/token 54 | response: 55 | body: 56 | string: '{"expires_in": 899,"token_type": "Bearer","access_token": "YjA5Mjg0MDAwMWQxYmU3NjJj","user_profile": 57 | "viewers","user_permissions": "[role-alarm-viewer,role-power-viewer,role-sensor-viewer,role-session-configure-self,role-system-info-viewer,role-user-configure-self,role-cli-access,role-system-info-viewer]"} 58 | 59 | 60 | ' 61 | headers: 62 | Cache-Control: 63 | - no-store 64 | Connection: 65 | - Keep-Alive 66 | Content-Length: 67 | - '309' 68 | Content-Security-Policy: 69 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 70 | 'self' 71 | Content-Type: 72 | - application/json;charset=UTF-8 73 | Date: 74 | - Wed, 24 Apr 2024 09:34:31 GMT 75 | Keep-Alive: 76 | - timeout=30s, max=999 77 | Pragma: 78 | - no-cache 79 | Server: 80 | - Tntnet 81 | Strict-Transport-Security: 82 | - max-age=31536000 83 | X-Content-Type-Options: 84 | - nosniff 85 | X-XSS-Protection: 86 | - '1' 87 | status: 88 | code: 200 89 | message: OK 90 | - request: 91 | body: null 92 | headers: 93 | Accept: 94 | - '*/*' 95 | Accept-Encoding: 96 | - gzip, deflate 97 | Authorization: 98 | - Bearer YjA5Mjg0MDAwMWQxYmU3NjJj 99 | Connection: 100 | - keep-alive 101 | User-Agent: 102 | - python-requests/2.31.0 103 | method: GET 104 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1 105 | response: 106 | body: 107 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1\",\n\t\"id\": 108 | \"1\",\n\t\"identification\": {\n\t\t\"uuid\": \"1e3e39e3-d95d-5113-b897-bd31290f82fb\",\n\t\t\"vendor\": 109 | \"EATON\",\n\t\t\"model\": \"Eaton 5P 1550\",\n\t\t\"serialNumber\": \"G117K46202\",\n\t\t\"type\": 110 | \"5P1550\",\n\t\t\"partNumber\": \"5P1550\",\n\t\t\"firmwareVersion\": \"02.14.0026\",\n\t\t\"name\": 111 | \"Eaton 5P 1550\",\n\t\t\"contact\": \"\",\n\t\t\"location\": \"\",\n\t\t\"interface\": 112 | \"/rest/mbdetnrs/1.0/powerDistributions/1\"\n\t},\n\t\"specification\": {\n\t\t\"type\": 113 | 1\n\t},\n\t\"configuration\": {\n\t\t\"nominalFrequency\": 50,\n\t\t\"nominalVoltage\": 114 | 230,\n\t\t\"nominalActivePower\": 1100,\n\t\t\"nominalApparentPower\": 1550,\n\t\t\"nominalPercentLoad\": 115 | 105\n\t},\n\t\"ups\": {\n\t\t\"mode\": 9,\n\t\t\"modeLevel\": 1,\n\t\t\"topology\": 116 | 1\n\t},\n\t\"status\": {\n\t\t\"operating\": 16,\n\t\t\"health\": 5\n\t},\n\t\"inputs\": 117 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs\"\n\t},\n\t\"avr\": 118 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/avr\"\n\t},\n\t\"outputs\": 119 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs\"\n\t},\n\t\"inverters\": 120 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inverters\"\n\t},\n\t\"chargers\": 121 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/chargers\"\n\t},\n\t\"backupSystem\": 122 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\"\n\t},\n\t\"bypass\": 123 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/bypass\"\n\t},\n\t\"rectifiers\": 124 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/rectifiers\"\n\t},\n\t\"outlets\": 125 | {\n\t\t\"members@count\": 3,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": 126 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/_SHkPSSPUVm3Uvme8XaZ2w\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 127 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/ntyTH9M9VHCqR4ONgALeUA\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"@id\": 128 | \"/rest/mbdetnrs/1.0/powerDistributions/1/outlets/KZDUq7PyUfuj5OpvnZr7Iw\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 129 | headers: 130 | Cache-Control: 131 | - no-store 132 | Connection: 133 | - Keep-Alive 134 | Content-Encoding: 135 | - gzip 136 | Content-Length: 137 | - '582' 138 | Content-Security-Policy: 139 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 140 | 'self' 141 | Content-Type: 142 | - application/json;charset=UTF-8 143 | Date: 144 | - Wed, 24 Apr 2024 09:34:31 GMT 145 | Keep-Alive: 146 | - timeout=30s, max=999 147 | Pragma: 148 | - no-cache 149 | Server: 150 | - Tntnet 151 | Strict-Transport-Security: 152 | - max-age=31536000 153 | X-Content-Type-Options: 154 | - nosniff 155 | X-XSS-Protection: 156 | - '1' 157 | status: 158 | code: 200 159 | message: OK 160 | - request: 161 | body: null 162 | headers: 163 | Accept: 164 | - '*/*' 165 | Accept-Encoding: 166 | - gzip, deflate 167 | Authorization: 168 | - Bearer YjA5Mjg0MDAwMWQxYmU3NjJj 169 | Connection: 170 | - keep-alive 171 | User-Agent: 172 | - python-requests/2.31.0 173 | method: GET 174 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1 175 | response: 176 | body: 177 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1\",\n\t\"id\": 178 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 179 | 227.6,\n\t\t\t\"current\": 0.1\n\t\t}\n\t},\n\t\"status\": {\n\t\t\"operating\": 180 | 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": {\n\t\t\"@id\": 181 | \"/rest/mbdetnrs/1.0/powerDistributions/1/inputs/1/phases\"\n\t}\n}\n" 182 | headers: 183 | Cache-Control: 184 | - no-store 185 | Connection: 186 | - Keep-Alive 187 | Content-Length: 188 | - '323' 189 | Content-Security-Policy: 190 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 191 | 'self' 192 | Content-Type: 193 | - application/json;charset=UTF-8 194 | Date: 195 | - Wed, 24 Apr 2024 09:34:31 GMT 196 | Keep-Alive: 197 | - timeout=30s, max=999 198 | Pragma: 199 | - no-cache 200 | Server: 201 | - Tntnet 202 | Strict-Transport-Security: 203 | - max-age=31536000 204 | X-Content-Type-Options: 205 | - nosniff 206 | X-XSS-Protection: 207 | - '1' 208 | status: 209 | code: 200 210 | message: OK 211 | - request: 212 | body: null 213 | headers: 214 | Accept: 215 | - '*/*' 216 | Accept-Encoding: 217 | - gzip, deflate 218 | Authorization: 219 | - Bearer YjA5Mjg0MDAwMWQxYmU3NjJj 220 | Connection: 221 | - keep-alive 222 | User-Agent: 223 | - python-requests/2.31.0 224 | method: GET 225 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1 226 | response: 227 | body: 228 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1\",\n\t\"id\": 229 | \"1\",\n\t\"measures\": {\n\t\t\"realtime\": {\n\t\t\t\"frequency\": 49.9,\n\t\t\t\"voltage\": 230 | 227.6,\n\t\t\t\"current\": 0,\n\t\t\t\"activePower\": 0,\n\t\t\t\"apparentPower\": 231 | 0,\n\t\t\t\"powerFactor\": 0,\n\t\t\t\"percentLoad\": 0\n\t\t}\n\t},\n\t\"status\": 232 | {\n\t\t\"operating\": 16,\n\t\t\"health\": 5,\n\t\t\"state\": 2\n\t},\n\t\"phases\": 233 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/outputs/1/phases\"\n\t}\n}\n" 234 | headers: 235 | Cache-Control: 236 | - no-store 237 | Connection: 238 | - Keep-Alive 239 | Content-Length: 240 | - '409' 241 | Content-Security-Policy: 242 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 243 | 'self' 244 | Content-Type: 245 | - application/json;charset=UTF-8 246 | Date: 247 | - Wed, 24 Apr 2024 09:34:32 GMT 248 | Keep-Alive: 249 | - timeout=30s, max=999 250 | Pragma: 251 | - no-cache 252 | Server: 253 | - Tntnet 254 | Strict-Transport-Security: 255 | - max-age=31536000 256 | X-Content-Type-Options: 257 | - nosniff 258 | X-XSS-Protection: 259 | - '1' 260 | status: 261 | code: 200 262 | message: OK 263 | - request: 264 | body: null 265 | headers: 266 | Accept: 267 | - '*/*' 268 | Accept-Encoding: 269 | - gzip, deflate 270 | Authorization: 271 | - Bearer YjA5Mjg0MDAwMWQxYmU3NjJj 272 | Connection: 273 | - keep-alive 274 | User-Agent: 275 | - python-requests/2.31.0 276 | method: GET 277 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem 278 | response: 279 | body: 280 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem\",\n\t\"powerBank\": 281 | {\n\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\"\n\t}\n}\n" 282 | headers: 283 | Cache-Control: 284 | - no-store 285 | Connection: 286 | - Keep-Alive 287 | Content-Length: 288 | - '161' 289 | Content-Security-Policy: 290 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 291 | 'self' 292 | Content-Type: 293 | - application/json;charset=UTF-8 294 | Date: 295 | - Wed, 24 Apr 2024 09:34:32 GMT 296 | Keep-Alive: 297 | - timeout=30s, max=999 298 | Pragma: 299 | - no-cache 300 | Server: 301 | - Tntnet 302 | Strict-Transport-Security: 303 | - max-age=31536000 304 | X-Content-Type-Options: 305 | - nosniff 306 | X-XSS-Protection: 307 | - '1' 308 | status: 309 | code: 200 310 | message: OK 311 | - request: 312 | body: null 313 | headers: 314 | Accept: 315 | - '*/*' 316 | Accept-Encoding: 317 | - gzip, deflate 318 | Authorization: 319 | - Bearer YjA5Mjg0MDAwMWQxYmU3NjJj 320 | Connection: 321 | - keep-alive 322 | User-Agent: 323 | - python-requests/2.31.0 324 | method: GET 325 | uri: https://address.to.ups1/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank 326 | response: 327 | body: 328 | string: "{\n\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank\",\n\t\"specification\": 329 | {\n\t\t\"technology\": \"batteries\",\n\t\t\"nominalVoltage\": 36,\n\t\t\"nominalChargeCapacity\": 330 | 32400,\n\t\t\"capacityUnit\": 0\n\t},\n\t\"configuration\": {\n\t\t\"lowChargeCapacityLimit\": 331 | 20,\n\t\t\"deepDischargeProtectionEnabled\": true\n\t},\n\t\"measures\": {\n\t\t\"voltage\": 332 | 38.8,\n\t\t\"remainingChargeCapacity\": 100,\n\t\t\"remainingTime\": 15284\n\t},\n\t\"status\": 333 | {\n\t\t\"operating\": 5,\n\t\t\"health\": 5,\n\t\t\"state\": 1,\n\t\t\"present\": 334 | true,\n\t\t\"fault\": false,\n\t\t\"warning\": false,\n\t\t\"low\": false,\n\t\t\"criticallyLow\": 335 | false\n\t},\n\t\"test\": {\n\t\t\"configuration\": {\n\t\t\t\"period\": 2592000\n\t\t},\n\t\t\"result\": 336 | {\n\t\t\t\"level\": 1,\n\t\t\t\"timeStamp\": 1712304878,\n\t\t\t\"value\": 337 | 1\n\t\t},\n\t\t\"status\": 4\n\t},\n\t\"lcm\": {\n\t\t\"replaceDate\": 1709270908,\n\t\t\"health\": 338 | 15,\n\t\t\"expired\": true\n\t},\n\t\"entities\": {\n\t\t\"members@count\": 339 | 1,\n\t\t\"members\": [\n\t\t\t{\n\t\t\t\t\"@id\": \"/rest/mbdetnrs/1.0/powerDistributions/1/backupSystem/powerBank/entities/S2VQRv4BWEKo-lgmRsRDVQ\"\n\t\t\t}\n\t\t]\n\t}\n}\n" 340 | headers: 341 | Cache-Control: 342 | - no-store 343 | Connection: 344 | - Keep-Alive 345 | Content-Length: 346 | - '971' 347 | Content-Security-Policy: 348 | - connect-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self'; frame-ancestors 349 | 'self' 350 | Content-Type: 351 | - application/json;charset=UTF-8 352 | Date: 353 | - Wed, 24 Apr 2024 09:34:32 GMT 354 | Keep-Alive: 355 | - timeout=30s, max=999 356 | Pragma: 357 | - no-cache 358 | Server: 359 | - Tntnet 360 | Strict-Transport-Security: 361 | - max-age=31536000 362 | X-Content-Type-Options: 363 | - nosniff 364 | X-XSS-Protection: 365 | - '1' 366 | status: 367 | code: 200 368 | message: OK 369 | version: 1 370 | -------------------------------------------------------------------------------- /tests/fixtures/dummy_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ups1_name": { 3 | "address": "https://address.to.ups1", 4 | "user": "username", 5 | "password": "password" 6 | }, 7 | "ups2_name": { 8 | "address": "https://address.to.ups2", 9 | "user": "username", 10 | "password": "password" 11 | } 12 | } -------------------------------------------------------------------------------- /tests/test_prometheus_api_exporter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing the Exporter using the UPSExporter and UPSMultiExporter. 3 | """ 4 | import pytest 5 | from . import first_ups_details 6 | from prometheus_eaton_ups_exporter.exporter import ( 7 | UPSExporter, 8 | UPSMultiExporter, 9 | ) 10 | 11 | 12 | # Create Multi Exporter 13 | @pytest.fixture(scope="function") 14 | def single_exporter(ups_scraper_conf) -> UPSExporter: 15 | address, auth, ups_name = first_ups_details(ups_scraper_conf) 16 | return UPSExporter( 17 | address, 18 | auth, 19 | ups_name, 20 | insecure=True, 21 | verbose=True 22 | ) 23 | 24 | 25 | # Create Multi Exporter 26 | @pytest.fixture(scope="function") 27 | def multi_exporter(ups_scraper_conf) -> UPSMultiExporter: 28 | return UPSMultiExporter( 29 | ups_scraper_conf, 30 | insecure=True, 31 | verbose=True 32 | ) 33 | 34 | 35 | # Create Multi Exporter 36 | @pytest.fixture(scope="function") 37 | def threading_multi_exporter(ups_scraper_conf) -> UPSMultiExporter: 38 | return UPSMultiExporter( 39 | ups_scraper_conf, 40 | insecure=True, 41 | verbose=True, 42 | threading=True 43 | ) 44 | 45 | 46 | @pytest.mark.vcr() 47 | def test_single_collect(ups_scraper_conf, single_exporter) -> None: 48 | names = [ 49 | 'eaton_ups_input_volts', 'eaton_ups_input_hertz', 50 | 'eaton_ups_input_amperes', 'eaton_ups_output_volts', 51 | 'eaton_ups_output_hertz', 'eaton_ups_output_amperes', 52 | 'eaton_ups_output_voltamperes', 'eaton_ups_output_watts', 53 | 'eaton_ups_output_power_factor', 'eaton_ups_output_load_ratio', 54 | 'eaton_ups_battery_volts', 'eaton_ups_battery_capacity_ratio', 55 | 'eaton_ups_battery_remaining_seconds', 'eaton_ups_battery_health' 56 | ] 57 | gauges = single_exporter.collect() 58 | ups_gauges = [next(gauges) for _ in names] 59 | ups_name = single_exporter.ups_scraper.name 60 | labels = [{'ups_id': ups_name} for _ in ups_gauges] 61 | gauge_names = [gauge.name for gauge in ups_gauges] 62 | gauge_labels = [gauge.samples[0].labels for gauge in ups_gauges] 63 | 64 | assert gauge_names == names 65 | assert gauge_labels == labels 66 | 67 | 68 | @pytest.mark.vcr() 69 | def test_multi_collect(ups_scraper_conf, multi_exporter) -> None: 70 | names = [ 71 | 'eaton_ups_input_volts', 'eaton_ups_input_hertz', 72 | 'eaton_ups_input_amperes', 'eaton_ups_output_volts', 73 | 'eaton_ups_output_hertz', 'eaton_ups_output_amperes', 74 | 'eaton_ups_output_voltamperes', 'eaton_ups_output_watts', 75 | 'eaton_ups_output_power_factor', 'eaton_ups_output_load_ratio', 76 | 'eaton_ups_battery_volts', 'eaton_ups_battery_capacity_ratio', 77 | 'eaton_ups_battery_remaining_seconds', 'eaton_ups_battery_health' 78 | ] 79 | gauges = multi_exporter.collect() 80 | for ups_name in ups_scraper_conf.keys(): 81 | ups_gauges = [next(gauges) for _ in names] 82 | 83 | labels = [{'ups_id': ups_name} for _ in ups_gauges] 84 | gauge_names = [gauge.name for gauge in ups_gauges] 85 | gauge_labels = [gauge.samples[0].labels for gauge in ups_gauges] 86 | 87 | assert gauge_names == names 88 | assert gauge_labels == labels 89 | 90 | 91 | @pytest.mark.vcr() 92 | @pytest.mark.skip("TODO: test threading with pytest-vcr") 93 | def test_collect_threading(ups_scraper_conf, threading_multi_exporter) -> None: 94 | names = [ 95 | 'eaton_ups_input_volts', 'eaton_ups_input_hertz', 96 | 'eaton_ups_input_amperes', 'eaton_ups_output_volts', 97 | 'eaton_ups_output_hertz', 'eaton_ups_output_amperes', 98 | 'eaton_ups_output_voltamperes', 'eaton_ups_output_watts', 99 | 'eaton_ups_output_power_factor', 'eaton_ups_output_load_ratio', 100 | 'eaton_ups_battery_volts', 'eaton_ups_battery_capacity_ratio', 101 | 'eaton_ups_battery_remaining_seconds', 'eaton_ups_battery_health' 102 | ] 103 | gauges = threading_multi_exporter.collect() 104 | while (gauge := next(gauges, None)) is not None: 105 | assert gauge.name in names 106 | assert gauge.samples[0].labels['ups_id'] in \ 107 | list(ups_scraper_conf.keys()) 108 | -------------------------------------------------------------------------------- /tests/test_ups_api_scraper.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from . import first_ups_details 3 | from prometheus_eaton_ups_exporter.scraper import UPSScraper 4 | from prometheus_eaton_ups_exporter.scraper_globals import ( 5 | AUTHENTICATION_FAILED, 6 | CERTIFICATE_VERIFY_FAILED, 7 | CONNECTION_ERROR, 8 | INVALID_URL_ERROR, 9 | LoginFailedException, 10 | MISSING_SCHEMA_ERROR, 11 | REST_API_PATH, 12 | TIMEOUT_ERROR, 13 | ) 14 | 15 | 16 | def ups_scraper(address, 17 | auth, 18 | name: str, 19 | insecure: bool = True) -> UPSScraper: 20 | return UPSScraper( 21 | address, 22 | auth, 23 | name, 24 | insecure=insecure, 25 | verbose=True 26 | ) 27 | 28 | 29 | @pytest.fixture(scope="function") 30 | def scraper_fixture(ups_scraper_conf): 31 | ups_name = list(ups_scraper_conf.keys())[0] 32 | address = ups_scraper_conf[ups_name]['address'] 33 | auth = ( 34 | ups_scraper_conf[ups_name]['user'], 35 | ups_scraper_conf[ups_name]['password'] 36 | ) 37 | return ups_scraper(address, auth, ups_name) 38 | 39 | 40 | @pytest.mark.vcr() 41 | def test_login(scraper_fixture) -> None: 42 | token_type, access_token = scraper_fixture.login() 43 | assert token_type == "Bearer" 44 | 45 | 46 | @pytest.mark.vcr() 47 | def test_load_rest_api(scraper_fixture) -> None: 48 | """Tests load_page function with rest api.""" 49 | request = scraper_fixture.load_page( 50 | scraper_fixture.ups_address + REST_API_PATH 51 | ) 52 | # Todo 53 | json_response = request.json() 54 | response_keys = [ 55 | '@id', 'id', 'identification', 'specification', 56 | 'configuration', 'ups', 'status', 'inputs', 57 | 'avr', 'outputs', 'inverters', 'chargers', 58 | 'backupSystem', 'bypass', 'rectifiers', 'outlets' 59 | ] 60 | assert response_keys == list(json_response.keys()) 61 | 62 | 63 | @pytest.mark.vcr() 64 | def test_get_measures(scraper_fixture) -> None: 65 | measures = scraper_fixture.get_measures() 66 | measures_keys = [ 67 | 'ups_id', 68 | 'ups_inputs', 69 | 'ups_outputs', 70 | 'ups_powerbank' 71 | ] 72 | assert list(measures.keys()) == measures_keys 73 | assert measures['ups_id'] == scraper_fixture.name 74 | 75 | ups_inputs = measures.get('ups_inputs') 76 | assert list(ups_inputs) == [ 77 | '@id', 'id', 'measures', 'status', 'phases' 78 | ] 79 | assert list(ups_inputs.get('measures')) == [ 80 | 'realtime' 81 | ] 82 | assert list(ups_inputs.get('measures').get('realtime')) == [ 83 | 'frequency', 'voltage', 'current' 84 | ] 85 | 86 | ups_outputs = measures.get('ups_outputs') 87 | assert list(ups_outputs) == [ 88 | '@id', 'id', 'measures', 'status', 'phases' 89 | ] 90 | assert list(ups_outputs.get('measures')) == [ 91 | 'realtime' 92 | ] 93 | assert list(ups_outputs.get('measures').get('realtime')) == [ 94 | 'frequency', 'voltage', 'current', 'activePower', 95 | 'apparentPower', 'powerFactor', 'percentLoad' 96 | ] 97 | 98 | ups_powerbank = measures.get('ups_powerbank') 99 | assert list(ups_powerbank) == [ 100 | '@id', 'specification', 'configuration', 101 | 'measures', 'status', 'test', 'lcm', 'entities' 102 | ] 103 | assert list(ups_powerbank.get('measures')) == [ 104 | 'voltage', 'remainingChargeCapacity', 'remainingTime' 105 | ] 106 | 107 | 108 | def test_missing_schema_exception() -> None: 109 | scraper = ups_scraper("", ("", ""), "") 110 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 111 | scraper.login() 112 | assert pytest_wrapped_e.type == LoginFailedException 113 | assert pytest_wrapped_e.value.error_code == MISSING_SCHEMA_ERROR 114 | 115 | 116 | def test_invalid_url_exception() -> None: 117 | scraper = ups_scraper("https:0.0.0.0", ("", ""), "") 118 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 119 | scraper.login() 120 | assert pytest_wrapped_e.type == LoginFailedException 121 | assert pytest_wrapped_e.value.error_code == INVALID_URL_ERROR 122 | 123 | 124 | def test_connection_refused_exception() -> None: 125 | scraper = ups_scraper("https://127.0.0.1", ("", ""), "") 126 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 127 | scraper.login() 128 | assert pytest_wrapped_e.type == LoginFailedException 129 | assert pytest_wrapped_e.value.error_code == CONNECTION_ERROR 130 | 131 | 132 | @pytest.mark.vcr() 133 | @pytest.mark.skip("Login-failure is not recorded by pytest-vcr") 134 | # This test does not create a vcr casette because the login fails (which is 135 | # what is tested). Therefor, this test needs a valid ups-address and can be run 136 | # locally with success, but not on git. 137 | def test_certificate_exception(ups_scraper_conf) -> None: 138 | address, auth, ups_name = first_ups_details(ups_scraper_conf) 139 | scraper = ups_scraper( 140 | address, 141 | auth, 142 | ups_name, 143 | insecure=False 144 | ) 145 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 146 | scraper.load_page(address) 147 | assert pytest_wrapped_e.type == LoginFailedException 148 | assert pytest_wrapped_e.value.error_code == CERTIFICATE_VERIFY_FAILED 149 | 150 | 151 | class MockLoginFailedException: 152 | def __init__(self, *args, **kwargs): 153 | raise LoginFailedException(*args, **kwargs) 154 | 155 | 156 | def test_certificate_exception_monkey_patch(monkeypatch, 157 | ups_scraper_conf) -> None: 158 | address, auth, ups_name = first_ups_details(ups_scraper_conf) 159 | scraper = ups_scraper( 160 | address, 161 | auth, 162 | ups_name, 163 | insecure=False 164 | ) 165 | 166 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 167 | monkeypatch.setattr( 168 | scraper, "load_page", 169 | MockLoginFailedException( 170 | CERTIFICATE_VERIFY_FAILED, 171 | "message" 172 | ) 173 | ) 174 | scraper.load_page(address) 175 | assert pytest_wrapped_e.type == LoginFailedException 176 | assert pytest_wrapped_e.value.error_code == CERTIFICATE_VERIFY_FAILED 177 | 178 | 179 | def test_login_timeout_exception(monkeypatch, ups_scraper_conf) -> None: 180 | address, _, ups_name = first_ups_details(ups_scraper_conf) 181 | scraper = ups_scraper( 182 | address, 183 | ("a", "b"), 184 | ups_name 185 | ) 186 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 187 | monkeypatch.setattr( 188 | scraper, "login", 189 | MockLoginFailedException( 190 | TIMEOUT_ERROR, 191 | "message" 192 | ) 193 | ) 194 | scraper.login() 195 | assert pytest_wrapped_e.type == LoginFailedException 196 | assert pytest_wrapped_e.value.error_code == TIMEOUT_ERROR 197 | 198 | 199 | def test_auth_failed_exception(monkeypatch, ups_scraper_conf) -> None: 200 | address, _, ups_name = first_ups_details(ups_scraper_conf) 201 | scraper = ups_scraper( 202 | address, 203 | (ups_scraper_conf[ups_name]['user'], "abc"), 204 | ups_name 205 | ) 206 | with pytest.raises(LoginFailedException) as pytest_wrapped_e: 207 | monkeypatch.setattr( 208 | scraper, "load_page", 209 | MockLoginFailedException( 210 | AUTHENTICATION_FAILED, 211 | "message" 212 | ) 213 | ) 214 | scraper.load_page(address + REST_API_PATH) 215 | assert pytest_wrapped_e.type == LoginFailedException 216 | assert pytest_wrapped_e.value.error_code == AUTHENTICATION_FAILED 217 | --------------------------------------------------------------------------------