├── .coveragerc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── configuration.rst │ ├── contributing.rst │ ├── index.rst │ ├── install.rst │ ├── intro.rst │ ├── modules.rst │ ├── options.rst │ ├── pyiso.rst │ ├── supporting.rst │ └── usage.rst ├── pyiso ├── __init__.py ├── aeso.py ├── base.py ├── bchydro.py ├── bpa.py ├── caiso.py ├── eia_esod.py ├── ercot.py ├── eu.py ├── ieso.py ├── isone.py ├── miso.py ├── nbpower.py ├── nlhydro.py ├── nspower.py ├── nvenergy.py ├── nyiso.py ├── pei.py ├── pjm.py ├── sask.py ├── spp.py ├── sveri.py ├── tasks.py └── yukon.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── fixtures ├── aeso │ └── latest_electricity_market_report.csv ├── bchydro │ └── data1.xls ├── bpa │ ├── WindGenTotalLoadYTD_2014_short.xls │ └── wind_tsv.csv ├── caiso │ ├── 20170312_DailyRenewablesWatch.txt │ ├── 20171104_DailyRenewablesWatch.txt │ ├── 20171105_DailyRenewablesWatch.txt │ ├── 20171106_DailyRenewablesWatch.txt │ ├── ene_slrs.xml │ ├── ren_report.csv │ ├── sld_forecast.xml │ ├── sld_ren_forecast.xml │ ├── systemconditions.html │ └── todays_outlook_renewables.html ├── ercot │ └── real_time_system_conditions.html ├── eu │ ├── de_gen.xml │ └── de_load.xml ├── ieso │ ├── full_Adequacy2_20170618.xml │ ├── full_IntertieScheduleFlow_20170630.xml │ ├── full_PredispConstTotals_20170708.xml │ ├── full_RealtimeConstTotals_2017070101.xml │ ├── reduced_GenOutputCapability_20160429.xml │ ├── reduced_GenOutputCapability_20160501.xml │ └── reduced_GenOutputbyFuelHourly_2016.xml ├── isone │ ├── morningreport_20160101.json │ ├── morningreport_current.json │ ├── sevendayforecast_20160101.json │ └── sevendayforecast_current.json ├── nbpower │ ├── 2017-03-12 00.csv │ ├── 2017-03-13 00.csv │ ├── 2017-07-16 22.csv │ ├── 2017-11-05 00.csv │ ├── 2017-11-06 00.csv │ └── SystemInformation_realtime.html ├── nlhydro │ └── system-information-center.html ├── nspower │ ├── currentload.json │ ├── currentmix.json │ └── forecast.json ├── nvenergy │ ├── Monthly Ties and Loads_L11_01_2017.html │ ├── native system load and ties12_24_2017.html │ └── tomorrow.htm ├── nyiso │ ├── 20160119rtfuelmix.csv │ ├── 20171122ExternalLimitsFlows.csv │ ├── 20171122isolf.csv │ ├── 20171122pal.csv │ ├── 20171122rtfuelmix.csv │ └── lmp.csv ├── pei │ └── chart-values.json ├── pjm │ ├── ForecastedLoadHistory.html │ └── InstantaneousLoad.html ├── sask │ └── sysloadJSON.json ├── sveri │ └── api_response.csv └── yukon │ ├── current_2017-10-11.html │ ├── hourly_2017-10-11.html │ └── hourly_2017-11-11.html ├── integration ├── __init__.py ├── integration_test_aeso.py ├── integration_test_bpa.py ├── integration_test_caiso.py ├── integration_test_eia.py ├── integration_test_ercot.py ├── integration_test_ieso.py ├── integration_test_isone.py ├── integration_test_nbpower.py ├── integration_test_nyiso.py ├── integration_test_pjm.py ├── integration_test_sask.py ├── test_genmix.py ├── test_load.py ├── test_tasks.py └── test_trade.py └── unit ├── __init__.py ├── test_aeso.py ├── test_base.py ├── test_bchydro.py ├── test_bpa.py ├── test_caiso.py ├── test_eia.py ├── test_ercot.py ├── test_eu.py ├── test_factory.py ├── test_ieso.py ├── test_isone.py ├── test_miso.py ├── test_nbpower.py ├── test_nlhydro.py ├── test_nspower.py ├── test_nvenergy.py ├── test_nyiso.py ├── test_pei.py ├── test_pjm.py ├── test_sask.py ├── test_sveri.py └── test_yukon.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | source= 4 | pyiso 5 | omit= 6 | */tests/* 7 | */tests.py 8 | *__init__.py 9 | */venv/* 10 | *.virtualenvs* 11 | *script.py 12 | 13 | [report] 14 | #CLI report config 15 | 16 | [html] 17 | #html report config 18 | directory = htmlcov 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.py[cod] 4 | *~ 5 | *.db 6 | celerybeat-schedule 7 | .env 8 | env.sh 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Packages 14 | *.egg 15 | .eggs 16 | *.egg-info 17 | dist 18 | build 19 | eggs 20 | parts 21 | bin 22 | var 23 | sdist 24 | develop-eggs 25 | .installed.cfg 26 | lib 27 | lib64 28 | __pycache__ 29 | 30 | # Installer logs 31 | pip-log.txt 32 | 33 | # Unit test / coverage reports 34 | .coverage 35 | .tox 36 | nosetests.xml 37 | htmlcov/* 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | 47 | # database files 48 | *.csv 49 | !/tests/**/*.csv 50 | *.dump 51 | *.sqlite 52 | 53 | # coverage files 54 | htmlcov 55 | 56 | # Certificate key files 57 | *.pem 58 | 59 | .DS_Store 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.4' 5 | 6 | install: 7 | # Install miniconda for python 2.7 8 | - wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; 9 | - bash miniconda.sh -b -p $HOME/miniconda 10 | - export PATH="$HOME/miniconda/bin:$PATH" 11 | - hash -r 12 | - conda config --set always_yes yes --set changeps1 no 13 | - conda update -q conda 14 | # Useful for debugging any issues with conda 15 | - conda info -a 16 | 17 | # Install 18 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION 19 | - conda install -n test-environment pandas>=0.18 requests==2.9.1 20 | - source activate test-environment 21 | - python setup.py install 22 | - pip install -r requirements.txt 23 | - pip install coveralls 24 | script: nosetests --with-coverage --cover-package=pyiso ./tests/unit/ 25 | deploy: 26 | provider: pypi 27 | user: WattTime 28 | password: 29 | secure: "Jiezhw/tS1ZOatEflgSlCB7cXp+gv5AGorflKIMAJF6O3wqGNh+Y+fcfwkUoGD2ODewnKS0fU8W/58EoX8vu+jRX84aIUIZcOdND5nflMz2kdjbglujKx4vt3DAR7eWRdk5UL69+Ts0GOwTGcj+fCHOud9eNiKXCGkxStrtfhr0=" 30 | on: 31 | tags: true 32 | repo: WattTime/pyiso 33 | after_success: 34 | - coveralls 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [Please find our contribution guidelines here] (docs/source/contributing.rst) 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyiso 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.org/WattTime/pyiso.svg?branch=master)](https://travis-ci.org/WattTime/pyiso) 5 | [![Coverage Status](https://coveralls.io/repos/WattTime/pyiso/badge.svg?branch=master)](https://coveralls.io/r/WattTime/pyiso?branch=master) 6 | [![PyPI version](https://badge.fury.io/py/pyiso.svg)](https://badge.fury.io/py/pyiso) 7 | 8 | pyiso provides Python client libraries for [ISO](https://www.epsa.org/industry/primer/?fa=rto) and other power grid data sources. 9 | It powers the WattTime API (https://api.watttime.org/), among other things. 10 | 11 | Documentation: https://pyiso.readthedocs.io/ 12 | 13 | User group: https://groups.google.com/forum/#!forum/pyiso-users 14 | 15 | Upcoming Changes 16 | ---------------- 17 | * Add changes here 18 | 19 | Changelog 20 | --------- 21 | * 0.4.0: Added BCH (trade-only), EIA, IESO, NBPower, NSPower, AESO, PEI, SASK, NLHydro and YUKON authorities. Added `ccgt` as fuel type. Removed 22 | `get_lmp` function (backward-incompatible change). 23 | * 0.3.19: Fix bug with `Biomass/Fossil` fuel type for BPA 24 | * 0.3.18: Fix bug with PJM date parsing 25 | * 0.3.17: Fix bug with `Black Liquor` fuel type for PJM 26 | * 0.3.16: Implement ISONE get_morningreport and get_sevendayforecast 27 | * 0.3.15: Minor bugfixes to CAISO get_generation. 28 | * 0.3.14: Minor bugfixes to ISONE, PJM, and ERCOT. 29 | * 0.3.13: Major feature: generation mix in PJM (RTHR market only). Minor change: SSL handling in BPA. 30 | * 0.3.12: Bugfix: fixed EU authentication, thanks @frgtn! 31 | * 0.3.11: Changes: `timeout_seconds` kwarg to client constructor; do not remember options from one `get_*` call to the next. 32 | * 0.3.10: Changes: Historical DAHR LMP data in NYISO is not available using `market='DAHR'`; error is raised when trying to access historical RT5M LMP data in PJM. 33 | * 0.3.9: Fixes breaking error with BeautifulSoup. Minor fixes: closes issues #79, #84. 34 | * 0.3.8: Minor feature: Historical NYISO LMP data available farther into the past. 35 | * 0.3.7: Change: For CAISO historical generation, defaults to DAHR market instead of RTHR if no market is provided. 36 | * 0.3.6: Change: If `forecast=True` is requested without specifying `start_at` or `end_at`, `start_at` will default to the current time; previously it defaulted to midnight in the ISO's local time. Bugfixes: times outside the `start_at`-`end_at` range are no longer returned for ISONE generation and load, CAISO DAHR generation. 37 | * 0.3.5: Minor feature: all tasks can accept strings for `start_at` and `end_at` kwargs. 38 | * 0.3.2: Minor feature: `get_lmp` task. Minor bugfixes: safer handling of response errors for load (BPA, ERCOT, MISO, NVEnergy, PJM) and generation (BPA, CAISO, ERCOT, ISONE, NYISO); clean up LMP tests. 39 | * 0.3.1: Minor changes for PJM real-time load data: fall back to OASIS if Data Snapshot is down, round time down to nearest 5 min period. Major feature: SVERI back up. 40 | * 0.3.0: Major features: Add LMP to all ISOs, license change. Please contact us for alternative licenses. Bugfixes: SVERI has a new URL. Minor features: CAISO has 15-minute RTPD market. 41 | * 0.2.23: Major fix: ERCOT real-time data format changed, this release is updated to match the new format. Minor fixes to excel date handling with pandas 0.18, and MISO forecast. 42 | * 0.2.22: Feature: LMP in NYISO, thanks @ecalifornica! Bug fixes for DST transition. 43 | * 0.2.21: Major feature: generation mix in NYISO. Bug fix: time zone handling in NYISO. 44 | * 0.2.18: Minor change: enforce pandas version 0.17 or higher. 45 | * 0.2.17: Minor change: Limit retries in `base.request`, and increase time between retries. 46 | * 0.2.16: Major fix: PJM deprecated the data source that was used in previous releases. This release uses a new data source that has load and tie flows, but not wind. So PJM generation mix has been deprecated for the moment--hopefully it will return in a future release. 47 | * 0.2.15: Minor changes: enforce pandas 0.16.2 and change NYISO index labelling to fix NYISO regression in some environments. 48 | * 0.2.14: Major features: forecast load in ERCOT, MISO, NYISO, PJM; forecast genmix in MISO; forecast trade in MISO. Minor changes: fixed DST bug in BPA, refactored several to better use pandas. 49 | * 0.2.13: Minor bugfix: Better able to find recent data in NVEnergy. 50 | * 0.2.12: Major features: EU support, support for throttling in CAISO. Minor upgrades: Improve docs, dedup logging messages. 51 | * 0.2.11: Minor bugfixes. Also, made a backward-incompatible change to the data structure that's returned from `get_ancillary_services` in CAISO. 52 | * 0.2.10: Fixed bug in CAISO LMP DAM. 53 | * 0.2.9: Added load and generation mix for SVERI (AZPS, DEAA, ELE, HGMA, IID, GRIF, PNM, SRP, TEPC, WALC) 54 | * 0.2.8: Added lmp in ISONE. Also, made a backward-incompatible change to the data structure that's returned from `get_lmp` in CAISO. 55 | * 0.2.7: Added load and trade in Nevada Energy (NEVP and SPPC) 56 | * 0.2.1: Added load (real-time 5-minute and hourly forecast) in ISONE 57 | * 0.2.0: Maintained Python 2.7 support and added Python 3.4! Thanks @emunsing 58 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | 5 | Accounts 6 | -------- 7 | 8 | ISONE requires a username and password to collect data. 9 | You can register for an ISONE account here (http://www.iso-ne.com/participate/applications-status-changes/access-software-systems#data-feeds) 10 | 11 | 12 | Then, set your usernames and passwords as environment variables: 13 | 14 | export ISONE_USERNAME=myusername1 15 | export ISONE_PASSWORD=mysecret1 16 | 17 | The EU (ENTSOe) REST API requires a security token. You must first sign up for an account and then get your security token from here (https://transparency.entsoe.eu/). To use the token set as an environment variable as follows: 18 | 19 | export ENTSOe_SECURITY_TOKEN=token 20 | 21 | The EIA API requires an API key. You can apply for a key here (https://www.eia.gov/opendata/register.cfm). To use the key, set an environment variable as follows: 22 | 23 | export EIA_KEY=my-eia-api-key 24 | 25 | All other ISOs allow unauthenticated users to collect data, so no other credentials are needed. 26 | 27 | 28 | Logging and debug 29 | ----------------- 30 | 31 | By default, logging occurs at the INFO level. If you want to change this, you can set the `LOG_LEVEL` environment variable to the `integer associated with the desired log level `_. For instance, ERROR is 40 and DEBUG is 10. 32 | 33 | You can also turn on DEBUG level logging by setting the `DEBUG` environment variable to a truthy value. This setting will additionally enable caching during testing, which will significantly speed up the test suite. 34 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Right now, pyiso only has interfaces for collecting a small subset of the interesting electricity data that the ISOs provide. 5 | You can help by adding more! 6 | Please create an issue on `github `_ 7 | if you have questions about any of this. 8 | 9 | 10 | For developers 11 | -------------- 12 | 13 | When you're ready to get started coding: 14 | 15 | * fork the `repo `_ 16 | * install in development mode: ``python setup.py develop`` 17 | * run the tests: ``python setup.py test`` (or ``python setup.py test -s tests.test_some_file.TestSomeClass.test_some_method`` to run a specific subset of the tests) 18 | * add tests to the :py:mod:`tests` directory and code to the :py:mod:`pyiso` directory, following the conventions that you see in the existing code 19 | * add docs to the `docs/source` directory 20 | * add a note to the Upcoming Changes section in `README.md` on a separate line 21 | * send a pull request 22 | * sign the CLA at https://www.clahub.com/agreements/WattTime/pyiso (see below) 23 | 24 | 25 | For data users 26 | -------------- 27 | 28 | Found a bug, or know of a data source that you think pyiso should include? 29 | Please add an issue to `github `_. 30 | Ideas of new balancing authorities (anywhere in the world) 31 | and of new data streams from ISOs we already support are both very welcome. 32 | 33 | 34 | For project admins 35 | ------------------ 36 | 37 | Before making a release, check that these are true in the master branch of the GitHub repo: 38 | 39 | * the changelog in `README.md` includes all changes since the last release 40 | * test coverage is good and the tests pass locally and on Travis 41 | * changes are reflected in the docs in `docs/source` 42 | * the version number is upgraded in `pyiso/__init__.py` 43 | 44 | To make a release, run these commands (replacing 0.x.y with the correct version number): 45 | 46 | .. code-block:: bash 47 | 48 | git checkout master 49 | git pull origin master 50 | git tag v0.x.y 51 | git push origin master --tags 52 | python setup.py sdist upload 53 | 54 | Releasing via twine: 55 | 56 | .. code-block:: bash 57 | 58 | python setup.py sdist 59 | twine upload dist/pyiso-VERSION.tar.gz 60 | 61 | Legal things 62 | ------------ 63 | 64 | Because we use pyiso as the base for our other software products, we ask that contributors sign the following Contributor License Agreement. If you have any questions, or concerns, please drop us a line on Github. 65 | 66 | 67 | You and WattTime, Corp, a california non-profit corporation, hereby accept and agree to the following terms and conditions: 68 | 69 | Your "Contributions" means all of your past, present and future contributions of object code, source code and documentation to pyiso however submitted to pyiso, excluding any submissions that are conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 70 | 71 | You hereby grant to the WattTime, Corp a non-exclusive, irrevocable, worldwide, no-charge, transferable copyright license to use, execute, prepare derivative works of, and distribute (internally and externally, in object code and, if included in your Contributions, source code form) your Contributions. Except for the rights granted to the WattTime, Corp in this paragraph, You reserve all right, title and interest in and to your Contributions. 72 | 73 | You represent that you are legally entitled to grant the above license. If your employer(s) have rights to intellectual property that you create, you represent that you have received permission to make the Contibutions on behalf of that employer, or that your employer has waived such rights for your Contributions to pyiso. 74 | 75 | You represent that, except as disclosed in your Contribution submission(s), each of your Contributions is your original creation. You represent that your Contribution submissions(s) included complete details of any license or other restriction (including, but not limited to, related patents and trademarks) associated with any part of your Contributions)(including a copy of any applicable license agreement). You agree to notify WattTime, Corp of any facts or circumstances of which you become aware that would make Your representations in the Agreement inaccurate in any respect. 76 | 77 | You are not expected to provide support for your Contributions, except to the extent you desire to provide support. Your may provide support for free, for a fee, or not at all. Your Contributions are provided as-is, with all faults, defects and errors, and without any warranty of any kind (either express or implied) including, without limitation, any implied warranty of merchantability and fitness for a particular purpose and any warranty of non-infringement. 78 | 79 | To get started, sign the Contributor License Agreement. 80 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pyiso documentation master file, created by 2 | sphinx-quickstart on Fri Mar 28 19:04:32 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to pyiso's documentation! 7 | ================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | intro 15 | install 16 | configuration 17 | usage 18 | options 19 | contributing 20 | supporting 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/source/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install 5 | ------- 6 | 7 | Pyiso is available on `PyPI `_ 8 | and on `GitHub `_. 9 | 10 | For users, the easiest way to get pyiso is with pip:: 11 | 12 | pip install pyiso 13 | 14 | For developers, you can get the source from `GitHub `_ 15 | or `PyPI `_, then:: 16 | 17 | cd pyiso 18 | python setup.py install 19 | 20 | Pyiso depends on pandas so be prepared for a large install. 21 | 22 | Windows Users: If you are unable to setup pyiso due to issues with installing or using numpy, a dependent package of pyiso, try installing a precompiled version of numpy found here: http://www.lfd.uci.edu/~gohlke/pythonlibs/ 23 | 24 | 25 | Uninstall 26 | --------- 27 | 28 | To uninstall:: 29 | 30 | pip uninstall pyiso 31 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | pyiso 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyiso 8 | -------------------------------------------------------------------------------- /docs/source/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ======= 3 | 4 | Not all date range options are available for all methods in all regions. 5 | Here's what's available now: 6 | 7 | ======================== ========== =================================== ============== ============ 8 | method ``latest`` ``start_at`` and ``end_at`` pair ``yesterday`` forecast ok 9 | ======================== ========== =================================== ============== ============ 10 | ``AESO.get_generation`` yes no no no 11 | ``AESO.get_load`` yes yes yes yes 12 | ``AESO.get_trade`` yes no no no 13 | ``BCH.get_generation`` no no no no 14 | ``BCH.get_load`` no no no no 15 | ``BCH.get_trade`` yes yes yes no 16 | ``BPA.get_generation`` yes yes no no 17 | ``BPA.get_load`` yes yes no no 18 | ``CAISO.get_generation`` yes yes yes yes 19 | ``CAISO.get_load`` yes yes yes yes 20 | ``CAISO.get_trade`` yes yes yes yes 21 | ``EIA.get_generation`` yes yes yes no 22 | ``EIA.get_load`` yes yes yes yes 23 | ``EIA.get_trade`` yes yes yes no 24 | ``ERCOT.get_generation`` yes no no no 25 | ``ERCOT.get_load`` yes yes no yes 26 | ``EU.get_generation`` yes yes yes no 27 | ``EU.get_load`` yes yes no yes 28 | ``IESO.get_generation`` yes yes yes yes 29 | ``IESO.get_load`` yes yes yes yes 30 | ``IESO.get_trade`` yes yes yes yes 31 | ``ISONE.get_generation`` yes yes no no 32 | ``ISONE.get_load`` yes yes no yes 33 | ``MISO.get_generation`` yes yes no yes 34 | ``MISO.get_load`` yes yes no yes 35 | ``MISO.get_trade`` no yes no yes 36 | ``NLH.get_generation`` no no no no 37 | ``NLH.get_load`` yes no no no 38 | ``NLH.get_trade`` no no no no 39 | ``NPB.get_generation`` no no no no 40 | ``NPB.get_load`` yes yes no yes 41 | ``NPB.get_trade`` yes no no no 42 | ``NSP.get_generation`` yes yes no no 43 | ``NSP.get_load`` yes yes no yes 44 | ``NSP.get_trade`` no no no no 45 | ``NVEnergy.get_load`` yes yes no yes 46 | ``NYISO.get_generation`` yes yes no no 47 | ``NYISO.get_load`` yes yes no yes 48 | ``NYISO.get_trade`` yes yes no no 49 | ``PEI.get_generation`` yes no no no 50 | ``PEI.get_load`` yes no no no 51 | ``PEI.get_trade`` no no no no 52 | ``PJM.get_generation`` yes no no no 53 | ``PJM.get_load`` yes yes no yes 54 | ``PJM.get_trade`` yes no no no 55 | ``SASK.get_generation`` no no no no 56 | ``SASK.get_load`` yes no no no 57 | ``SASK.get_trade`` no no no no 58 | ``SVERI.get_generation`` yes yes no no 59 | ``SVERI.get_load`` yes yes no no 60 | ``YUKON.get_generation`` yes yes no no 61 | ``YUKON.get_load`` yes yes no no 62 | ``YUKON.get_trade`` n/a n/a n/a n/a 63 | ======================== ========== =================================== ============== ============ 64 | -------------------------------------------------------------------------------- /docs/source/pyiso.rst: -------------------------------------------------------------------------------- 1 | pyiso package 2 | ============= 3 | 4 | Module contents 5 | --------------- 6 | 7 | .. automodule:: pyiso 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | pyiso.tasks module 13 | ------------------ 14 | 15 | .. automodule:: pyiso.tasks 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | 21 | Submodules 22 | ---------- 23 | 24 | .. automodule:: pyiso.base 25 | :members: 26 | :undoc-members: 27 | 28 | .. automodule:: pyiso.bpa 29 | :members: 30 | :undoc-members: 31 | 32 | .. automodule:: pyiso.caiso 33 | :members: 34 | :undoc-members: 35 | 36 | .. automodule:: pyiso.eia_esod 37 | :members: 38 | :undoc-members: 39 | 40 | .. automodule:: pyiso.ercot 41 | :members: 42 | :undoc-members: 43 | 44 | .. automodule:: pyiso.ieso 45 | :members: 46 | :undoc-members: 47 | 48 | .. automodule:: pyiso.isone 49 | :members: 50 | :undoc-members: 51 | 52 | .. automodule:: pyiso.miso 53 | :members: 54 | :undoc-members: 55 | 56 | .. automodule:: pyiso.pjm 57 | :members: 58 | :undoc-members: 59 | 60 | .. automodule:: pyiso.spp 61 | :members: 62 | :undoc-members: 63 | -------------------------------------------------------------------------------- /docs/source/supporting.rst: -------------------------------------------------------------------------------- 1 | Supporting 2 | ========== 3 | 4 | Pyiso is an open source project maintained by `WattTime `_, 5 | a nonprofit that develops software standards to 6 | reduce power grid pollution and enable new kinds of clean energy choices. 7 | 8 | We've spent more than 1000 developer-hours 9 | building pyiso, keeping it up-to-date with evolving data sources, 10 | and adding features requested by the community. 11 | As the foundation of our internal data pipeline, 12 | it makes our work easier every day. 13 | And we've made it free and open source because we want to 14 | make open energy data access a bit easier 15 | for other researchers, engineers, and citizens too! 16 | 17 | Want to chip in and support pyiso? 18 | You or your company can make a tax-deductible donation 19 | to WattTime `here `_. 20 | Every dollar helps us help you! 21 | We also have corporate sponsorship opportunities available; 22 | `get in touch `_ if you're interested. 23 | 24 | Another great way to support pyiso is to send us a quick 25 | `thank-you note `_. 26 | Your testimonials help us raise money from other folks, 27 | so it really does make a difference. Thanks bunches! 28 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | There are two main ways to use pyiso: via the client objects, or via celery tasks. 5 | The client approach is preferred for scripted data analysis. 6 | The task approach enables asynchronous or periodic data collection 7 | and is in use at the `WattTime Impact API `_. 8 | 9 | Clients 10 | ------- 11 | 12 | .. py:currentmodule:: pyiso.base 13 | 14 | First, create a client using the ``client_factory(ba_name)`` function. 15 | ``ba_name`` should be taken from this list of abbreviated names for available balancing authorities 16 | listed on the :doc:`intro` page. 17 | For example:: 18 | 19 | >>> from pyiso import client_factory 20 | >>> isone = client_factory('ISONE') 21 | 22 | 23 | Requests made to external data sources will automatically time out after 20 seconds. 24 | To change this value, add a keyword argument in the constructor:: 25 | 26 | >>> isone = client_factory('ISONE', timeout_seconds=60) 27 | 28 | 29 | Each client returned by ``client_factory`` is derived from :py:class:`BaseClient` and provides one or more of the following methods (see also :doc:`options`): 30 | 31 | .. automethod:: BaseClient.get_generation 32 | :noindex: 33 | 34 | .. automethod:: BaseClient.get_load 35 | :noindex: 36 | 37 | .. automethod:: BaseClient.get_trade 38 | :noindex: 39 | 40 | The lists returned by clients are conveniently structured for import into other data structures like :py:class:`pandas.DataFrame`:: 41 | 42 | >>> import pandas as pd 43 | >>> data = isone.get_generation(latest=True) 44 | >>> df = pd.DataFrame(data) 45 | >>> print df 46 | ba_name freq fuel_name gen_MW market timestamp 47 | 0 ISONE n/a coal 1170.0 RT5M 2014-03-29 20:40:27+00:00 48 | 1 ISONE n/a hydro 813.8 RT5M 2014-03-29 20:40:27+00:00 49 | 2 ISONE n/a natgas 4815.7 RT5M 2014-03-29 20:40:27+00:00 50 | 3 ISONE n/a nuclear 4618.8 RT5M 2014-03-29 20:40:27+00:00 51 | 4 ISONE n/a biogas 29.5 RT5M 2014-03-29 20:40:27+00:00 52 | 5 ISONE n/a refuse 428.6 RT5M 2014-03-29 20:40:27+00:00 53 | 6 ISONE n/a wind 85.8 RT5M 2014-03-29 20:40:27+00:00 54 | 7 ISONE n/a biomass 434.3 RT5M 2014-03-29 20:40:27+00:00 55 | 56 | Happy data analysis! 57 | 58 | 59 | Tasks 60 | ----- 61 | 62 | If you have a `celery `_ environment set up, you can use the tasks provided in the :py:mod:`pyiso.tasks` module. 63 | There is one task for each of the client's ``get_*`` methods that implements a thin wrapper around that method. 64 | The call signatures match those of the corresponding client methods, except that the ``ba_name`` is a required first argument. 65 | For example, to get the latest ISONE generation mix data every 10 minutes, 66 | add this to your `celerybeat schedule `_:: 67 | 68 | CELERYBEAT_SCHEDULE = { 69 | 'get-isone-genmix-latest' : { 70 | 'task': 'pyiso.tasks.get_generation', 71 | 'schedule': crontab(minute='*/10'), 72 | 'args': ['ISONE'], 73 | 'kwargs': {'latest': True}, 74 | } 75 | } 76 | 77 | In practice, you will want to chain these tasks with something that captures and processes their output. 78 | -------------------------------------------------------------------------------- /pyiso/__init__.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import logging 3 | import os.path 4 | import sys 5 | from os import environ 6 | 7 | __version__ = '0.4.0' 8 | 9 | 10 | # ERROR = 40, WARNING = 30, INFO = 20, DEBUG = 10 11 | LOG_LEVEL = int(environ.get('LOG_LEVEL', logging.INFO)) 12 | 13 | 14 | # logger: create here to only add the handler once! 15 | LOGGER = logging.getLogger(__name__) 16 | handler = logging.StreamHandler(sys.stdout) 17 | LOGGER.addHandler(handler) 18 | LOGGER.setLevel(LOG_LEVEL) 19 | 20 | BALANCING_AUTHORITIES = { 21 | 'AESO': {'class': 'AESOClient', 'module': 'aeso'}, 22 | 'AZPS': {'class': 'SVERIClient', 'module': 'sveri'}, 23 | 'BCH': {'class': 'BCHydroClient', 'module': 'bchydro'}, 24 | 'BPA': {'class': 'BPAClient', 'module': 'bpa'}, 25 | 'CAISO': {'class': 'CAISOClient', 'module': 'caiso'}, 26 | 'DEAA': {'class': 'SVERIClient', 'module': 'sveri'}, 27 | 'EIA': {'class': 'EIAClient', 'module': 'eia_esod'}, 28 | 'ELE': {'class': 'SVERIClient', 'module': 'sveri'}, 29 | 'ERCOT': {'class': 'ERCOTClient', 'module': 'ercot'}, 30 | 'EU': {'class': 'EUClient', 'module': 'eu'}, 31 | 'GRIF': {'class': 'SVERIClient', 'module': 'sveri'}, 32 | 'HGMA': {'class': 'SVERIClient', 'module': 'sveri'}, 33 | 'IESO': {'class': 'IESOClient', 'module': 'ieso'}, 34 | 'IID': {'class': 'SVERIClient', 'module': 'sveri'}, 35 | 'ISONE': {'class': 'ISONEClient', 'module': 'isone'}, 36 | 'MISO': {'class': 'MISOClient', 'module': 'miso'}, 37 | 'NBP': {'class': 'NBPowerClient', 'module': 'nbpower'}, 38 | 'NLH': {'class': 'NLHydroClient', 'module': 'nlhydro'}, 39 | 'NEVP': {'class': 'NVEnergyClient', 'module': 'nvenergy'}, 40 | 'NSP': {'class': 'NSPowerClient', 'module': 'nspower'}, 41 | 'NYISO': {'class': 'NYISOClient', 'module': 'nyiso'}, 42 | 'PEI': {'class': 'PEIClient', 'module': 'pei'}, 43 | 'PJM': {'class': 'PJMClient', 'module': 'pjm'}, 44 | 'PNM': {'class': 'SVERIClient', 'module': 'sveri'}, 45 | 'SASK': {'class': 'SaskPowerClient', 'module': 'sask'}, 46 | 'SPPC': {'class': 'NVEnergyClient', 'module': 'nvenergy'}, 47 | 'SRP': {'class': 'SVERIClient', 'module': 'sveri'}, 48 | 'TEPC': {'class': 'SVERIClient', 'module': 'sveri'}, 49 | 'WALC': {'class': 'SVERIClient', 'module': 'sveri'}, 50 | 'YUKON': {'class': 'YukonEnergyClient', 'module': 'yukon'}, 51 | } 52 | 53 | 54 | def client_factory(client_name, **kwargs): 55 | """Return a client for an external data set""" 56 | # set up 57 | dir_name = os.path.dirname(os.path.abspath(__file__)) 58 | error_msg = 'No client found for name %s' % client_name 59 | client_key = client_name.upper() 60 | 61 | # find client 62 | try: 63 | client_vals = BALANCING_AUTHORITIES[client_key] 64 | module_name = client_vals['module'] 65 | 66 | class_name = client_vals['class'] 67 | except KeyError: 68 | raise ValueError(error_msg) 69 | 70 | # find module 71 | try: 72 | fp, pathname, description = imp.find_module(module_name, [dir_name]) 73 | except ImportError: 74 | raise ValueError(error_msg) 75 | 76 | # load 77 | try: 78 | mod = imp.load_module(module_name, fp, pathname, description) 79 | finally: 80 | # Since we may exit via an exception, close fp explicitly. 81 | if fp: 82 | fp.close() 83 | 84 | # instantiate class 85 | try: 86 | client_inst = getattr(mod, class_name)(**kwargs) 87 | except AttributeError: 88 | raise ValueError(error_msg) 89 | 90 | # set name 91 | client_inst.NAME = client_name 92 | 93 | return client_inst 94 | -------------------------------------------------------------------------------- /pyiso/bchydro.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from datetime import timedelta 3 | from pandas import DataFrame 4 | from pandas import Timestamp 5 | from pyiso import LOGGER 6 | from pyiso.base import BaseClient 7 | 8 | 9 | class BCHydroClient(BaseClient): 10 | """ 11 | British Columbia Hydro (Canada) has a public transmission flow page available at 12 | https://www.bchydro.com/bctc/system_cms/actual_flow/data.aspx 13 | 14 | This client provides five-minute, historical trade data up to 9 days in the past through to the current time 15 | (i.e. most recent five minute interval). 16 | """ 17 | NAME = 'BCH' 18 | TZ_NAME = 'Canada/Pacific' 19 | 20 | def __init__(self): 21 | super(BCHydroClient, self).__init__() 22 | self.bc_tz = pytz.timezone(self.TZ_NAME) 23 | self.bc_now = self.local_now() 24 | 25 | def handle_options(self, **kwargs): 26 | super(BCHydroClient, self).handle_options(**kwargs) 27 | self.options['earliest_data_at'] = self.bc_now - timedelta(days=9) 28 | self.options['latest_data_at'] = self.bc_now 29 | 30 | def get_generation(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 31 | pass 32 | 33 | def get_load(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 34 | pass 35 | 36 | def get_trade(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 37 | self.handle_options(latest=latest, yesterday=yesterday, start_at=start_at, end_at=end_at, data='trade') 38 | trades = [] 39 | if latest: 40 | self._trade_latest(trades) 41 | elif self._is_valid_date_range(): 42 | self._trade_historical(trades) 43 | else: 44 | if self.options.get('forecast', False): 45 | LOGGER.warn(self.NAME + ': Trade forecasts are not supported.') 46 | else: 47 | msg = '%s: Requested date range %s to %s is outside range of available data from %s to %s.' % \ 48 | (self.NAME, self.options.get('start_at', None), self.options.get('end_at', None), 49 | self.options.get('earliest_data_at', None), self.options.get('latest_data_at', None)) 50 | LOGGER.warn(msg) 51 | return trades 52 | 53 | def _actual_flow_data(self): 54 | """ 55 | Requests the "Actual Flow" Excel and processes the response. 56 | :return: The "Actual Flow" Excel spreadsheet as a pandas timeseries. 57 | :rtype: pandas.DataFrame 58 | """ 59 | data = self.fetch_xls('https://www.bchydro.com/bctc/system_cms/actual_flow/data1.xls') 60 | if data: 61 | actual_flow_df = data.parse('Sheet1') 62 | return actual_flow_df 63 | else: 64 | return DataFrame() 65 | 66 | def _is_valid_date_range(self): 67 | """ 68 | Checks whether the start_at and end_at options provided lie within the supported date range. Assumes that 69 | self.handle_options(...) has already been called. 70 | :return: True/False indicating whether to requested date range is valid. 71 | :rtype: bool 72 | """ 73 | if self.options['start_at'] > self.options['latest_data_at'] or \ 74 | self.options['end_at'] < self.options['earliest_data_at']: 75 | return False 76 | else: 77 | return True 78 | 79 | def _trade_historical(self, trades): 80 | """ 81 | Requests historical import/export trade data and appends results in pyiso format to the provided list for those 82 | results which fall between start_at and end_at time range. 83 | :param list trades: A list to append pyiso-formatted results to. 84 | """ 85 | actual_flow_df = self._actual_flow_data() 86 | for idx, row in actual_flow_df.iterrows(): 87 | local_flow_dt = row['Time'].to_pydatetime() 88 | local_flow_dt = self.bc_tz.localize(local_flow_dt) 89 | us_flow = row['BC-US Actual'] 90 | ab_flow = row['BC-AB Actual'] 91 | sum_flow = us_flow + ab_flow 92 | if self.options['start_at'] <= local_flow_dt <= self.options['end_at']: 93 | self._append_trade(result_ts=trades, tz_aware_dt=local_flow_dt, net_exp_mw=sum_flow) 94 | 95 | def _trade_latest(self, trades): 96 | """ 97 | Requests the latest import/export trade data and appends results in pyiso format to the provided list. 98 | :param list trades: The pyiso results list to append results to. 99 | """ 100 | actual_flow_df = self._actual_flow_data() 101 | if len(actual_flow_df) > 0: 102 | latest_flow = actual_flow_df.tail(1) 103 | local_flow_dt = latest_flow.iloc[0]['Time'].to_pydatetime() 104 | local_flow_dt = self.bc_tz.localize(local_flow_dt) 105 | us_flow = latest_flow.iloc[0]['BC-US Actual'] 106 | ab_flow = latest_flow.iloc[0]['BC-AB Actual'] 107 | sum_flow = us_flow + ab_flow 108 | self._append_trade(result_ts=trades, tz_aware_dt=local_flow_dt, net_exp_mw=sum_flow) 109 | 110 | def _append_trade(self, result_ts, tz_aware_dt, net_exp_mw): 111 | """ 112 | Appends a dict to the results list, with the keys [ba_name, timestamp, freq, market, net_exp_MW]. Timestamps 113 | are in UTC. 114 | :param list result_ts: The timeseries (a list of dicts) which results should be appended to. 115 | :param datetime tz_aware_dt: The datetime of the data being appended (timezone-aware). 116 | :param float net_exp_mw: The net exported megawatts (MW) (i.e. export - import). Negative values indicate that 117 | more electricity was imported than exported. 118 | """ 119 | result_ts.append({ 120 | 'ba_name': self.NAME, 121 | 'timestamp': Timestamp(tz_aware_dt.astimezone(pytz.utc)), 122 | 'freq': self.FREQUENCY_CHOICES.fivemin, 123 | 'market': self.MARKET_CHOICES.fivemin, 124 | 'net_exp_MW': net_exp_mw 125 | }) 126 | -------------------------------------------------------------------------------- /pyiso/bpa.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import pytz 3 | from dateutil.parser import parse as dateutil_parse 4 | import pandas as pd 5 | from pyiso.base import BaseClient 6 | from pyiso import LOGGER 7 | 8 | 9 | class BPAClient(BaseClient): 10 | NAME = 'BPA' 11 | 12 | base_url = 'https://transmission.bpa.gov/business/operations/' 13 | 14 | fuels = { 15 | 'Hydro': 'hydro', 16 | 'Wind': 'wind', 17 | 'Thermal': 'thermal', 18 | 'Fossil/Biomass': 'biomass', 19 | } 20 | 21 | TZ_NAME = 'America/Los_Angeles' 22 | 23 | def fetch_historical(self): 24 | """Get BPA generation or load data from the far past""" 25 | # set up requests 26 | request_urls = [] 27 | this_year = self.options['start_at'].year 28 | while this_year <= self.options['start_at'].year: 29 | if this_year >= 2011: 30 | request_urls.append(self.base_url + 'wind/WindGenTotalLoadYTD_%d.xls' % (this_year)) 31 | else: 32 | raise ValueError('Cannot get BPA generation data before 2011.') 33 | this_year += 1 34 | 35 | # set up columns to get 36 | mode = self.options['data'] 37 | if mode == 'gen': 38 | cols = [0, 2, 4, 5] 39 | header_names = ['Wind', 'Hydro', 'Thermal'] 40 | elif mode == 'load': 41 | cols = [0, 3] 42 | header_names = ['Load'] 43 | else: 44 | raise ValueError('Cannot fetch data without a data mode') 45 | 46 | # get each year of data 47 | pieces = [] 48 | for url in request_urls: 49 | xd = self.fetch_xls(url) 50 | piece = self.parse_to_df(xd, mode='xls', sheet_names=xd.sheet_names, 51 | skiprows=18, parse_cols=cols, 52 | index_col=0, parse_dates=True, 53 | header_names=header_names) 54 | pieces.append(piece) 55 | 56 | # return 57 | df = pd.concat(pieces) 58 | return df 59 | 60 | def fetch_recent(self): 61 | """Get BPA generation or load data from the past week""" 62 | # request text file 63 | response = self.request(self.base_url + 'wind/baltwg.txt') 64 | 65 | # set up columns to get 66 | mode = self.options['data'] 67 | if mode == 'gen': 68 | cols = [0, 2, 3, 4] 69 | elif mode == 'load': 70 | cols = [0, 1] 71 | else: 72 | raise ValueError('Cannot fetch data without a data mode') 73 | 74 | # parse like tsv 75 | if response: 76 | df = self.parse_to_df(response.text, skiprows=6, header=0, delimiter='\t', 77 | index_col=0, usecols=cols, 78 | date_parser=self.date_parser) 79 | else: 80 | LOGGER.warn('No recent data found for BPA %s' % self.options) 81 | df = pd.DataFrame() 82 | 83 | return df 84 | 85 | def date_parser(self, ts_str): 86 | ts = dateutil_parse(ts_str) 87 | return ts 88 | 89 | def fetcher(self): 90 | """Choose the correct fetcher method for this request""" 91 | # get mode from options 92 | mode = self.options.get('data') 93 | 94 | if mode in ['gen', 'load']: 95 | # default: latest or recent 96 | fetcher = self.fetch_recent 97 | if self.options.get('sliceable', None): 98 | if self.options['start_at'] < pytz.utc.localize(datetime.today() - timedelta(days=7)): 99 | # far past 100 | fetcher = self.fetch_historical 101 | 102 | else: 103 | raise ValueError('Cannot choose a fetcher without a data mode') 104 | 105 | return fetcher 106 | 107 | def parse_generation(self, df): 108 | # process times 109 | df.index = self.utcify_index(df.index) 110 | sliced = self.slice_times(df) 111 | 112 | # original header is fuel names 113 | sliced.rename(columns=self.fuels, inplace=True) 114 | pivoted = self.unpivot(sliced) 115 | pivoted.rename(columns={'level_1': 'fuel_name', 0: 'gen_MW'}, inplace=True) 116 | 117 | for fuel in pivoted['fuel_name'].unique(): 118 | if fuel not in self.fuels.values(): 119 | raise ValueError("Unhandled fuel type %s" % fuel) 120 | 121 | 122 | # return 123 | return pivoted 124 | 125 | def handle_options(self, **kwargs): 126 | # default handler 127 | super(BPAClient, self).handle_options(**kwargs) 128 | 129 | # check kwargs 130 | market = self.options.get('market', self.MARKET_CHOICES.fivemin) 131 | if market != self.MARKET_CHOICES.fivemin: 132 | raise ValueError('Market must be %s' % self.MARKET_CHOICES.fivemin) 133 | 134 | def get_generation(self, latest=False, start_at=False, end_at=False, **kwargs): 135 | # set args 136 | self.handle_options(data='gen', latest=latest, start_at=start_at, end_at=end_at, **kwargs) 137 | 138 | # fetch dataframe of data 139 | df = self.fetcher()() 140 | 141 | # return empty list if null 142 | if len(df) == 0: 143 | return [] 144 | 145 | # parse and clean 146 | cleaned_df = self.parse_generation(df) 147 | 148 | # serialize and return 149 | return self.serialize(cleaned_df, 150 | header=['timestamp', 'fuel_name', 'gen_MW'], 151 | extras={'ba_name': self.NAME, 152 | 'market': self.MARKET_CHOICES.fivemin, 153 | 'freq': self.FREQUENCY_CHOICES.fivemin}) 154 | 155 | def get_load(self, latest=False, start_at=False, end_at=False, **kwargs): 156 | # set args 157 | self.handle_options(data='load', latest=latest, start_at=start_at, end_at=end_at, **kwargs) 158 | 159 | # fetch dataframe of data 160 | df = self.fetcher()() 161 | 162 | # return empty list if null 163 | if len(df) == 0: 164 | return [] 165 | 166 | # parse and clean 167 | df.index = self.utcify_index(df.index) 168 | cleaned_df = self.slice_times(df) 169 | 170 | # serialize and return 171 | return self.serialize(cleaned_df, 172 | header=['timestamp', 'load_MW'], 173 | extras={'ba_name': self.NAME, 174 | 'market': self.MARKET_CHOICES.fivemin, 175 | 'freq': self.FREQUENCY_CHOICES.fivemin}) 176 | -------------------------------------------------------------------------------- /pyiso/nlhydro.py: -------------------------------------------------------------------------------- 1 | import re 2 | import warnings 3 | import pytz 4 | from datetime import datetime 5 | from bs4 import BeautifulSoup 6 | from pyiso.base import BaseClient 7 | from pandas import Timestamp 8 | 9 | 10 | class NLHydroClient(BaseClient): 11 | """ 12 | Client for Newfoundland and Labrador Hydro (Canada) which parses latest system load data from 13 | https://www.nlhydro.com/system-information/system-information-center/ 14 | """ 15 | 16 | NAME = 'NLH' 17 | TZ_NAME = 'Canada/Newfoundland' 18 | SYSTEM_INFO_URL = 'https://www.nlhydro.com/system-information/system-information-center' 19 | 20 | def __init__(self): 21 | super(NLHydroClient, self).__init__() 22 | self.nl_tz = pytz.timezone(self.TZ_NAME) 23 | self.nl_now = self.local_now() 24 | 25 | def get_generation(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 26 | pass 27 | 28 | def get_load(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 29 | self.handle_options(latest=latest, yesterday=yesterday, start_at=start_at, end_at=end_at) 30 | if latest: 31 | return self.get_latest_load() 32 | else: 33 | warnings.warn(message='%s only supports the latest=True argument for retrieving load data.' % self.NAME, 34 | category=UserWarning) 35 | return [] 36 | 37 | def get_trade(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 38 | pass 39 | 40 | def get_latest_load(self): 41 | """ 42 | Requests the "Current Island System Generation" HTML and returns it in pyiso load format. 43 | :return: List of dicts, each with keys ``[ba_name, timestamp, freq, market, load_MW]``. 44 | Timestamps are in UTC. 45 | :rtype: list 46 | """ 47 | response = self.request(self.SYSTEM_INFO_URL) 48 | if response and response.content: 49 | soup = BeautifulSoup(response.content, 'html.parser') 50 | sysgen_div = soup.find(name='div', attrs={'id': 'sysgen'}) 51 | sysgen_children = sysgen_div.find_all(name='p') 52 | load_p = sysgen_children[0] 53 | current_island_gen = float(re.search('\d+', load_p.string).group()) 54 | datetime_ele = sysgen_children[1].find('span').string 55 | last_updated = self.nl_tz.localize(datetime.strptime(datetime_ele, '%m/%d/%Y %I:%M %p')) 56 | 57 | return [{ 58 | 'ba_name': self.NAME, 59 | 'timestamp': Timestamp(last_updated.astimezone(pytz.utc)), 60 | 'freq': self.FREQUENCY_CHOICES.hourly, 61 | 'market': self.MARKET_CHOICES.hourly, 62 | 'load_MW': current_island_gen 63 | }] 64 | else: 65 | return [] 66 | -------------------------------------------------------------------------------- /pyiso/pei.py: -------------------------------------------------------------------------------- 1 | import json 2 | import warnings 3 | import pytz 4 | from datetime import datetime 5 | from pandas import Timestamp 6 | from pyiso.base import BaseClient 7 | 8 | 9 | class PEIClient(BaseClient): 10 | """ 11 | The government of Prince Edward Island, Canada publishes a "Wind Energy" summary page for the island at 12 | http://www.gov.pe.ca/windenergy/chart.php The latest load and a basic generation mix can be derived from this data. 13 | """ 14 | 15 | NAME = 'PEI' 16 | TZ_NAME = 'Etc/GMT+4' # Times are always given in Atlantic Standard Time 17 | 18 | def __init__(self): 19 | super(PEIClient, self).__init__() 20 | self.pei_tz = pytz.timezone(self.TZ_NAME) 21 | self.chart_values_url = 'http://www.gov.pe.ca/windenergy/chart-values.php' 22 | 23 | def get_generation(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 24 | self.handle_options(latest=latest, yesterday=yesterday, start_at=start_at, end_at=end_at, **kwargs) 25 | if latest: 26 | return self.get_latest_generation() 27 | else: 28 | warnings.warn(message='PEIClient only supports the latest=True argument for retrieving generation mix.', 29 | category=UserWarning) 30 | 31 | def get_load(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 32 | self.handle_options(latest=latest, yesterday=yesterday, start_at=start_at, end_at=end_at, **kwargs) 33 | if latest: 34 | return self.get_latest_load() 35 | else: 36 | warnings.warn(message='PEIClient only supports the latest=True argument for retrieving load data.', 37 | category=UserWarning) 38 | 39 | def get_trade(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 40 | pass 41 | 42 | def get_latest_load(self): 43 | """ 44 | Requests the JSON backing PEI's public "Wind Energy" page (http://www.gov.pe.ca/windenergy/chart.php) 45 | and returns it in pyiso load format. 46 | :return: List of dicts, each with keys ``[ba_name, timestamp, freq, market, load_MW]``. 47 | Timestamps are in UTC. 48 | :rtype: list 49 | """ 50 | loads = [] 51 | response = self.request(self.chart_values_url) 52 | if response: 53 | sysload_list = json.loads(response.content.decode('utf-8')) 54 | sysload_json = sysload_list[0] 55 | seconds_epoch = int(sysload_json.get('updateDate', None)) 56 | last_updated = datetime.fromtimestamp(timestamp=seconds_epoch, tz=self.pei_tz) 57 | total_on_island_load = float(sysload_json.get('data1', None)) 58 | loads.append({ 59 | 'ba_name': self.NAME, 60 | 'timestamp': Timestamp(last_updated.astimezone(pytz.utc)), 61 | 'freq': self.FREQUENCY_CHOICES.tenmin, 62 | 'market': self.MARKET_CHOICES.tenmin, 63 | 'load_MW': total_on_island_load 64 | }) 65 | return loads 66 | 67 | def get_latest_generation(self): 68 | """ 69 | Requests the JSON backing PEI's public "Wind Energy" page (http://www.gov.pe.ca/windenergy/chart.php) 70 | and returns it in pyiso generation mix format. 71 | :return: List of dicts, each with keys ``[ba_name, timestamp, freq, market, fuel_name, gen_MW]``. 72 | Timestamps are in UTC. 73 | :rtype: list 74 | """ 75 | genmix = [] 76 | response = self.request(self.chart_values_url) 77 | if response: 78 | sysload_list = json.loads(response.content.decode('utf-8')) 79 | sysload_json = sysload_list[0] 80 | seconds_epoch = int(sysload_json.get('updateDate', None)) 81 | last_updated_utc = datetime.fromtimestamp(timestamp=seconds_epoch, tz=pytz.utc) 82 | total_on_island_load = float(sysload_json.get('data1', None)) 83 | wind_mw = float(sysload_json.get('data2', None)) 84 | oil_mw = float(sysload_json.get('data3', None)) 85 | other_mw = total_on_island_load - wind_mw - oil_mw 86 | self._append_generation(generation_ts=genmix, utc_dt=last_updated_utc, fuel_name='oil', gen_mw=oil_mw) 87 | self._append_generation(generation_ts=genmix, utc_dt=last_updated_utc, fuel_name='other', gen_mw=other_mw) 88 | self._append_generation(generation_ts=genmix, utc_dt=last_updated_utc, fuel_name='wind', gen_mw=wind_mw) 89 | return genmix 90 | 91 | def _append_generation(self, generation_ts, utc_dt, fuel_name, gen_mw): 92 | generation_ts.append({ 93 | 'ba_name': self.NAME, 94 | 'timestamp': Timestamp(utc_dt), 95 | 'freq': self.FREQUENCY_CHOICES.tenmin, 96 | 'market': self.MARKET_CHOICES.tenmin, 97 | 'fuel_name': fuel_name, 98 | 'gen_MW': gen_mw 99 | }) 100 | -------------------------------------------------------------------------------- /pyiso/sask.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytz 3 | import warnings 4 | from datetime import datetime 5 | from pandas import Timestamp 6 | from pyiso.base import BaseClient 7 | 8 | 9 | class SaskPowerClient(BaseClient): 10 | NAME = 'SASK' 11 | TZ_NAME = 'Canada/Saskatchewan' 12 | SYSLOAD_URL = 'http://www.saskpower.com/spfeeds.nsf/feeds/sysloadJSON' 13 | 14 | def __init__(self): 15 | super(SaskPowerClient, self).__init__() 16 | self.sask_tz = pytz.timezone(self.TZ_NAME) 17 | 18 | def get_generation(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 19 | pass 20 | 21 | def get_load(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 22 | if latest: 23 | return self.get_latest_load() 24 | else: 25 | warnings.warn(message='SaskPowerClient only supports the latest=True argument for retrieving load data.', 26 | category=UserWarning) 27 | 28 | def get_trade(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 29 | pass 30 | 31 | def get_latest_load(self): 32 | """ 33 | Requests the "Current System Load" JSON from Sask Power's public data feeds and returns it in pyiso load format. 34 | :return: List of dicts, each with keys ``[ba_name, timestamp, freq, market, load_MW]``. 35 | Timestamps are in UTC. 36 | :rtype: list 37 | """ 38 | response = self.request(self.SYSLOAD_URL) 39 | sysload_json = json.loads(response.content.decode('utf-8')) 40 | updated_str = sysload_json.get('updatedTS', None) 41 | last_updated = self.sask_tz.localize(datetime.strptime(updated_str.upper(), '%Y-%m-%d %I:%M %p')) 42 | current_sysload = float(sysload_json.get('currentSysLoad', None)) 43 | return [{ 44 | 'ba_name': self.NAME, 45 | 'timestamp': Timestamp(last_updated.astimezone(pytz.utc)), 46 | 'freq': self.FREQUENCY_CHOICES.fivemin, 47 | 'market': self.MARKET_CHOICES.fivemin, 48 | 'load_MW': current_sysload 49 | }] 50 | 51 | -------------------------------------------------------------------------------- /pyiso/spp.py: -------------------------------------------------------------------------------- 1 | from pyiso.base import BaseClient 2 | 3 | 4 | class SPPClient(BaseClient): 5 | NAME = 'SPP' 6 | TZ_NAME = 'America/Chicago' 7 | base_url = 'https://marketplace.spp.org/web/guest/' 8 | 9 | def get_fuels(self, year=2014): 10 | if year == 2014: 11 | return { 12 | 'COAL': 'coal', 13 | 'FUEL_OIL': 'oil', 14 | 'GAS': 'natgas', 15 | 'HYDRO': 'hydro', 16 | 'NUCLEAR': 'nuclear', 17 | 'OTHER': 'other', 18 | 'PUMP_HYDRO': 'smhydro', 19 | 'SOLAR': 'solar', 20 | 'WASTE': 'refuse', 21 | 'WIND': 'wind', 22 | } 23 | else: 24 | return { 25 | 'COAL': 'coal', 26 | 'HYDRO': 'hydro', 27 | 'GAS': 'natgas', 28 | 'NUCLEAR': 'nuclear', 29 | 'DFO': 'oil', 30 | 'WIND': 'wind', 31 | } 32 | -------------------------------------------------------------------------------- /pyiso/sveri.py: -------------------------------------------------------------------------------- 1 | from pyiso.base import BaseClient 2 | from pyiso import LOGGER 3 | from datetime import datetime, timedelta 4 | from dateutil.parser import parse as dateutil_parse 5 | import pandas as pd 6 | import pytz 7 | 8 | 9 | class SVERIClient(BaseClient): 10 | """ 11 | Interface to SVERI data sources. 12 | 13 | For information about the data sources, 14 | see https://sveri.uaren.org/#howto 15 | """ 16 | NAME = 'SVERI' 17 | TZ_NAME = 'America/Phoenix' 18 | BASE_URL = 'https://sveri.energy.arizona.edu/api?' 19 | 20 | fuels = { 21 | 'Solar Aggregate (MW)': 'solar', 22 | 'Wind Aggregate (MW)': 'wind', 23 | 'Other Renewables Aggregate (MW)': 'renewable', 24 | 'Hydro Aggregate (MW)': 'hydro', 25 | 'Coal Aggregate (MW)': 'coal', 26 | 'Gas Aggregate (MW)': 'natgas', 27 | 'Other Fossil Fuels Aggregate (MW)': 'fossil', 28 | 'Nuclear Aggregate (MW)': 'nuclear', 29 | } 30 | 31 | def _get_payload(self, ids): 32 | if self.options['latest']: 33 | now = datetime.now(pytz.timezone(self.TZ_NAME)) 34 | start = now.strftime('%Y-%m-%d') 35 | end = (now + timedelta(days=1)).strftime('%Y-%m-%d') 36 | else: 37 | start = self.options['start_at'].astimezone(pytz.timezone(self.TZ_NAME)).strftime('%Y-%m-%d') 38 | end = self.options['end_at'].astimezone(pytz.timezone(self.TZ_NAME)).strftime('%Y-%m-%d') 39 | return { 40 | 'ids': ids, 41 | 'startDate': start, 42 | 'endDate': end, 43 | 'saveData': 'true' 44 | } 45 | 46 | def get_gen_payloads(self): 47 | p1 = self._get_payload('1,2,3,4') 48 | p2 = self._get_payload('5,6,7,8') 49 | return (p1, p2) 50 | 51 | def get_load_payload(self): 52 | return self._get_payload('0') 53 | 54 | def clean_df(self, df): 55 | # take only data at 5 minute marks 56 | df = df[df.index.second == 5] 57 | df = df[df.index.minute % 5 == 0] 58 | # unpivot and rename 59 | if self.options['data'] == 'gen': 60 | df.rename(columns=self.fuels, inplace=True) 61 | df = self.unpivot(df) 62 | df.rename(columns={'level_1': 'fuel_name', 0: 'gen_MW'}, inplace=True) 63 | else: 64 | df.rename(columns={"Load Aggregate (MW)": "load_MW"}, inplace=True) 65 | 66 | df.index.names = ['timestamp'] 67 | 68 | # change timestamps to utc and slice 69 | df.index = self.utcify_index(df.index) 70 | sliced = self.slice_times(df) 71 | return sliced 72 | 73 | def _clean_and_serialize(self, df): 74 | # if no data, nothing to do 75 | if len(df) == 0: 76 | return [] 77 | 78 | # clean 79 | cleaned_df = self.clean_df(df) 80 | 81 | # serialize 82 | extras = { 83 | 'ba_name': self.NAME, 84 | 'market': self.MARKET_CHOICES.fivemin, 85 | 'freq': self.FREQUENCY_CHOICES.fivemin 86 | } 87 | return self.serialize_faster(cleaned_df, extras) 88 | 89 | def get_generation(self, latest=False, yesterday=False, 90 | start_at=False, end_at=False, **kwargs): 91 | # set args 92 | self.handle_options(data='gen', latest=latest, yesterday=yesterday, 93 | start_at=start_at, end_at=end_at, **kwargs) 94 | self.no_forecast_warn() 95 | 96 | # fetch data 97 | payloads = self.get_gen_payloads() 98 | response = self.request(self.BASE_URL, params=payloads[0]) 99 | response2 = self.request(self.BASE_URL, params=payloads[1]) 100 | if not response or not response2: 101 | return [] 102 | 103 | if response.text == 'Invalid ids string.' or response2.text == 'Invalid ids string': 104 | return [] 105 | 106 | # parse 107 | df = self.parse_to_df(response.content, header=0, 108 | parse_dates=True, date_parser=self.date_parser, index_col=0) 109 | df2 = self.parse_to_df(response2.content, header=0, 110 | parse_dates=True, date_parser=self.date_parser, index_col=0) 111 | df = pd.concat([df, df2], axis=1, join='inner') 112 | 113 | # clean and serialize 114 | return self._clean_and_serialize(df) 115 | 116 | def get_load(self, latest=False, yesterday=False, start_at=False, end_at=False, **kwargs): 117 | # set args 118 | self.handle_options(data='load', latest=latest, yesterday=yesterday, 119 | start_at=start_at, end_at=end_at, **kwargs) 120 | self.no_forecast_warn() 121 | 122 | # fetch data 123 | payload = self.get_load_payload() 124 | response = self.request(self.BASE_URL, params=payload) 125 | if not response: 126 | return [] 127 | 128 | # parse 129 | df = self.parse_to_df(response.content, header=0, parse_dates=True, date_parser=self.date_parser, index_col=0) 130 | 131 | # clean and serialize 132 | return self._clean_and_serialize(df) 133 | 134 | def date_parser(self, ts_str): 135 | TZINFOS = { 136 | 'MST': pytz.timezone(self.TZ_NAME), 137 | } 138 | 139 | return dateutil_parse(ts_str, tzinfos=TZINFOS) 140 | 141 | def no_forecast_warn(self): 142 | if not self.options['latest'] and self.options['start_at'] >= pytz.utc.localize(datetime.utcnow()): 143 | LOGGER.warn("SVERI does not have forecast data. There will be no data for the chosen time frame.") 144 | -------------------------------------------------------------------------------- /pyiso/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from celery import shared_task 3 | from pyiso import client_factory 4 | import logging 5 | from datetime import datetime 6 | 7 | 8 | # set up logger 9 | logger = logging.getLogger(__name__) 10 | 11 | @shared_task 12 | def get_generation(ba_name, **kwargs): 13 | # get data 14 | c = client_factory(ba_name) 15 | data = c.get_generation(**kwargs) 16 | 17 | # log 18 | if len(data) == 0: 19 | msg = '%s: No generation data at %s with args %s' % (ba_name, datetime.utcnow().isoformat(), 20 | kwargs) 21 | logger.warn(msg) 22 | 23 | # return 24 | return data 25 | 26 | @shared_task 27 | def get_load(ba_name, **kwargs): 28 | # get data 29 | c = client_factory(ba_name) 30 | data = c.get_load(**kwargs) 31 | 32 | # log 33 | if len(data) == 0: 34 | msg = '%s: No load data at %s with args %s' % (ba_name, datetime.utcnow().isoformat(), 35 | kwargs) 36 | logger.warn(msg) 37 | 38 | # return 39 | return data 40 | 41 | 42 | @shared_task 43 | def get_trade(ba_name, **kwargs): 44 | # get data 45 | c = client_factory(ba_name) 46 | data = c.get_trade(**kwargs) 47 | 48 | # log 49 | if len(data) == 0: 50 | msg = '%s: No trade data at %s with args %s' % (ba_name, datetime.utcnow().isoformat(), 51 | kwargs) 52 | logger.warn(msg) 53 | 54 | # return 55 | return data 56 | 57 | 58 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.5.0 2 | celery>=3.1 3 | certifi 4 | freezegun 5 | html5lib 6 | libfaketime 7 | lxml>=3.6.4 8 | mock 9 | nose-timer 10 | nose 11 | pandas>=0.18,<0.21 12 | parameterized 13 | python-dateutil 14 | pytz 15 | requests-cache 16 | requests-mock 17 | requests 18 | Sphinx 19 | xlrd -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import codecs 3 | import os 4 | import re 5 | 6 | # to release: 7 | # python setup.py register sdist bdist_egg upload 8 | 9 | 10 | here = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | 13 | # Read the version number from a source file. 14 | # Why read it, and not import? 15 | # see https://groups.google.com/d/topic/pypa-dev/0PkjVpcxTzQ/discussion 16 | # https://github.com/pypa/sampleproject/blob/master/setup.py 17 | def find_version(*file_paths): 18 | # Open in Latin-1 so that we avoid encoding errors. 19 | # Use codecs.open for Python 2 compatibility 20 | with codecs.open(os.path.join(here, *file_paths), 'r', 'latin1') as f: 21 | version_file = f.read() 22 | 23 | # The version line must have the form 24 | # __version__ = 'ver' 25 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 26 | version_file, re.M) 27 | if version_match: 28 | return version_match.group(1) 29 | raise RuntimeError("Unable to find version string.") 30 | 31 | 32 | # Get the long description from the relevant file 33 | with codecs.open('README.md', encoding='utf-8') as f: 34 | long_description = f.read() 35 | 36 | 37 | setup( 38 | name='pyiso', 39 | packages=['pyiso'], 40 | version=find_version('pyiso', '__init__.py'), 41 | description='Python client libraries for ISO and other power grid data sources.', 42 | long_description=long_description, 43 | author='Anna Schneider', 44 | author_email='dev@watttime.org', 45 | maintainer='WattTime Corporation', 46 | maintainer_email='dev@watttime.org', 47 | url='https://github.com/WattTime/pyiso', 48 | license='AGPL v3', 49 | classifiers=[ 50 | 'Programming Language :: Python', 51 | 'Programming Language :: Python :: 2', 52 | 'Programming Language :: Python :: 3', 53 | 'License :: OSI Approved :: GNU Affero General Public License v3', 54 | 'Operating System :: OS Independent', 55 | 'Development Status :: 3 - Alpha', 56 | 'Environment :: Web Environment', 57 | 'Intended Audience :: Developers', 58 | 'Intended Audience :: Science/Research', 59 | 'Topic :: Internet :: WWW/HTTP', 60 | 'Topic :: Scientific/Engineering', 61 | 'Topic :: Scientific/Engineering :: Information Analysis', 62 | 'Topic :: Software Development :: Libraries', 63 | 'Topic :: Software Development :: Libraries :: Python Modules', 64 | ], 65 | test_suite='nose.collector', 66 | install_requires=[ 67 | 'beautifulsoup4>=4.5.0', 68 | 'pandas>=0.18,<0.21', 69 | 'python-dateutil', 70 | 'pytz', 71 | 'requests', 72 | 'celery>=3.1', 73 | 'xlrd', 74 | 'lxml>=3.6.4', 75 | 'html5lib', 76 | 'mock', 77 | 'certifi' 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def fixture_path(ba_name, filename): 5 | """ 6 | :param str ba_name: The balancing authority module name. 7 | :param str filename: The fixture file you wish to find the path of. 8 | :return: The full path to the test file within the fixtures directory, regardless of working directory. 9 | :rtype: str 10 | """ 11 | fixtures_base_path = os.path.join(os.path.dirname(__file__), './fixtures', ba_name.lower()) 12 | return os.path.join(fixtures_base_path, filename) 13 | 14 | 15 | def read_fixture(ba_name, filename, as_bytes=False): 16 | """ 17 | :param str ba_name: The balancing authority module name. 18 | :param str filename: The fixture file (interpreted as a raw string literal) you wish to load. 19 | :param bool as_bytes: Indicates whether file should open with 'read bytes' mode. 20 | :return: The file's content. 21 | """ 22 | file_path = fixture_path(ba_name=ba_name, filename=filename) 23 | mode = 'rb' if as_bytes else 'r' 24 | fixture_file = open(file_path, mode=mode) 25 | data = fixture_file.read() 26 | if hasattr(data, 'decode'): 27 | data = getattr(data, 'decode')(encoding='utf-8', errors='replace') 28 | fixture_file.close() 29 | return data 30 | -------------------------------------------------------------------------------- /tests/fixtures/aeso/latest_electricity_market_report.csv: -------------------------------------------------------------------------------- 1 | Current Supply Demand Report 2 | 3 | 4 | "Last Update : Jul 13, 2017 19:26" 5 | 6 | "Alberta Total Net Generation","10054" 7 | "Net Actual Interchange","-216" 8 | "Alberta Internal Load (AIL)","10270" 9 | "Net-To-Grid Generation","7844" 10 | "Contingency Reserve Required","477" 11 | "Dispatched Contingency Reserve (DCR)","482" 12 | "Dispatched Contingency Reserve -Gen","464" 13 | "Dispatched Contingency Reserve -Other","18" 14 | "LSSi Armed Dispatch","0" 15 | "LSSi Offered Volume","160" 16 | 17 | "COAL","6283","4504","0" 18 | "GAS","7348","4321","190" 19 | "HYDRO","894","426","264" 20 | "OTHER","434","261","10" 21 | "WIND","1445","542","0" 22 | "TOTAL","16404","10054","464" 23 | 24 | "British Columbia","-216" 25 | "Montana","0" 26 | "Saskatchewan","0" 27 | "TOTAL","-216" 28 | 29 | "Battle River #3 (BR3)","149","0","0" 30 | "Battle River #4 (BR4)","155","0","0" 31 | "Battle River #5 (BR5)","385","153","0" 32 | "Genesee #1 (GN1)","400","399","0" 33 | "Genesee #2 (GN2)","400","394","0" 34 | "Genesee #3 (GN3)","466","462","0" 35 | "H.R. Milner (HRM)","144","0","0" 36 | "Keephills #1 (KH1)","395","396","0" 37 | "Keephills #2 (KH2)","395","393","0" 38 | "Keephills #3 (KH3)","463","155","0" 39 | "Sheerness #1 (SH1)","400","372","0" 40 | "Sheerness #2 (SH2)","390","82","0" 41 | "Sundance #1 (SD1)","280","220","0" 42 | "Sundance #2 (SD2)","280","43","0" 43 | "Sundance #3 (SD3)","368","351","0" 44 | "Sundance #4 (SD4)","406","354","0" 45 | "Sundance #5 (SD5)","406","357","0" 46 | "Sundance #6 (SD6)","401","373","0" 47 | 48 | "
Simple Cycle
" 49 | "ASSET","MC","TNG","DCR" 50 | "AB Newsprint (ANC1)","63","0","0" 51 | "AltaGas Bantry (ALP1)","7","0","0" 52 | "AltaGas Parkland (ALP2)","10","0","0" 53 | "Carson Creek (GEN5)","15","0","0" 54 | "Cloverbar #1 (ENC1)","48","0","0" 55 | "Cloverbar #2 (ENC2)","101","98","0" 56 | "Cloverbar #3 (ENC3)","101","0","0" 57 | "Crossfield Energy Centre #1 (CRS1)","48","37","0" 58 | "Crossfield Energy Centre #2 (CRS2)","48","37","0" 59 | "Crossfield Energy Centre #3 (CRS3)","48","38","0" 60 | "Drywood (DRW1)","6","0","5" 61 | "High River (MFG1)*","16","0","0" 62 | "House Mountain (HSM1)","6","0","0" 63 | "Judy Creek (GEN6)","15","0","0" 64 | "Lethbridge Burdett (ME03)","7","0","0" 65 | "Lethbridge Coaldale (ME04)","6","0","0" 66 | "Lethbridge Taber (ME02)","8","0","0" 67 | "NPC1 Denis St. Pierre (NPC1)","11","0","0" 68 | "NPC2 JL Landry (NPC2)","9","0","0" 69 | "Northern Prairie Power Project (NPP1)","105","73","0" 70 | "Poplar Hill #1 (PH1)","48","0","10" 71 | "Rainbow #5 (RB5)","50","0","0" 72 | "Ralston (NAT1)","20","0","0" 73 | "Valley View 1 (VVW1)","50","0","0" 74 | "Valley View 2 (VVW2)","50","0","33" 75 | "West Cadotte (WCD1)","20","17","0" 76 | "
Cogeneration
" 77 | "ASSET","MC","TNG","DCR" 78 | "ATCO Scotford Upgrader (APS1)","195","112","15" 79 | "Air Liquide Scotford #1 (ALS1)","96","66","10" 80 | "AltaGas Harmattan (HMT1)","45","16","0" 81 | "Base Plant (SCR1)","50","9","0" 82 | "Bear Creek 1 (BCRK)","64","52","0" 83 | "Bear Creek 2 (BCR2)","36","18","0" 84 | "BuckLake (PW01)","5","0","0" 85 | "CNRL Horizon (CNR5)*","203","163","0" 86 | "Camrose (CRG1)*","10","8","0" 87 | "Carseland Cogen (TC01)","95","0","0" 88 | "Christina Lake (CL01)","101","75","0" 89 | "Dow Hydrocarbon (DOWG)","326","131","35" 90 | "Edson (TLM2)","13","8","0" 91 | "Firebag (SCR6)","473","299","0" 92 | "Foster Creek (EC04)","98","71","0" 93 | "Joffre #1 (JOF1)","474","196","15" 94 | "Kearl (IOR3)","84","29","0" 95 | "Lindbergh (PEC1)*","16","10","0" 96 | "MEG1 Christina Lake (MEG1)","202","140","0" 97 | "MacKay River (MKRC)","197","171","0" 98 | "Mahkeses (IOR1)","180","146","0" 99 | "Muskeg River (MKR1)","202","155","7" 100 | "Nabiye (IOR2)*","195","146","0" 101 | "Nexen Inc #2 (NX02)","220","154","0" 102 | "Poplar Creek (SCR5)","376","267","0" 103 | "Primrose #1 (PR1)","100","72","0" 104 | "Rainbow Lake #1 (RL1)","47","0","0" 105 | "Redwater Cogen (TC02)","46","35","0" 106 | "Shell Caroline (SHCG)*","19","0","0" 107 | "Syncrude #1 (SCL1)*","510","291","0" 108 | "U of C Generator (UOC1)*","12","11","0" 109 | "University of Alberta (UOA1)*","39","5","15" 110 | "
Combined Cycle
" 111 | "ASSET","MC","TNG","DCR" 112 | "Cavalier (EC01)","120","94","5" 113 | "ENMAX Calgary Energy Centre (CAL1)","320","264","0" 114 | "Fort Nelson (FNG1)","73","0","0" 115 | "Medicine Hat #1 (CMH1)","210","153","30" 116 | "Nexen Inc #1 (NX01)","120","0","10" 117 | "Shepard (EGC1)","860","654","0" 118 | 119 | "Bighorn Hydro (BIG)","120","108","0" 120 | "Bow River Hydro (BOW1)","320","174","61" 121 | "Brazeau Hydro (BRA)","350","48","203" 122 | "CUPC Oldman River (OMRH)","32","32","0" 123 | "Chin Chute (CHIN)","15","11","0" 124 | "Dickson Dam (DKSN)","15","14","0" 125 | "Irrican Hydro (ICP1)","7","7","0" 126 | "Raymond Reservoir (RYMD)","21","19","0" 127 | "Taylor Hydro (TAY1)","14","13","0" 128 | 129 | "Ardenville Wind (ARD1)*","68","34","0" 130 | "BUL1 Bull Creek (BUL1)*","13","4","0" 131 | "BUL2 Bull Creek (BUL2)*","16","1","0" 132 | "Blackspring Ridge (BSR1)*","300","164","0" 133 | "Blue Trail Wind (BTR1)*","66","25","0" 134 | "Castle River #1 (CR1)*","39","16","0" 135 | "Castle Rock Wind Farm (CRR1)*","77","30","0" 136 | "Cowley Ridge (CRE3)*","20","11","0" 137 | "Enmax Taber (TAB1)*","81","17","0" 138 | "Ghost Pine (NEP1)*","82","3","0" 139 | "Halkirk Wind Power Facility (HAL1)*","150","9","0" 140 | "Kettles Hill (KHW1)*","63","27","0" 141 | "McBride Lake Windfarm (AKE1)*","73","20","0" 142 | "Oldman 2 Wind Farm 1 (OWF1)*","46","31","0" 143 | "Soderglen Wind (GWW1)*","71","50","0" 144 | "Summerview 1 (IEW1)*","66","58","0" 145 | "Summerview 2 (IEW2)*","66","34","0" 146 | "Suncor Chin Chute (SCR3)*","30","3","0" 147 | "Suncor Magrath (SCR2)*","30","5","0" 148 | "Wintering Hills (SCR4)*","88","0","0" 149 | 150 | "APF Athabasca (AFG1)*","131","59","0" 151 | "Cancarb Medicine Hat (CCMH)","42","26","0" 152 | "DAI1 Daishowa (DAI1)","52","41","10" 153 | "Drayton Valley (DV1)","11","0","0" 154 | "Gold Creek Facility (GOC1)","5","0","0" 155 | "Grande Prairie EcoPower (GPEC)","27","12","0" 156 | "NRGreen (NRG3)","16","10","0" 157 | "Slave Lake (SLP1)*","9","6","0" 158 | "Weldwood #1 (WWD1)*","50","41","0" 159 | "Westlock (WST1)","18","1","0" 160 | "Weyerhaeuser (WEY1)","48","41","0" 161 | "Whitecourt Power (EAGL)","25","24","0" 162 | 163 | -------------------------------------------------------------------------------- /tests/fixtures/bchydro/data1.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WattTime/pyiso/c336708daf1ebbd196f5e09e706a7949f28e7fba/tests/fixtures/bchydro/data1.xls -------------------------------------------------------------------------------- /tests/fixtures/bpa/WindGenTotalLoadYTD_2014_short.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WattTime/pyiso/c336708daf1ebbd196f5e09e706a7949f28e7fba/tests/fixtures/bpa/WindGenTotalLoadYTD_2014_short.xls -------------------------------------------------------------------------------- /tests/fixtures/bpa/wind_tsv.csv: -------------------------------------------------------------------------------- 1 | BPA Balancing Authority Load & Total Wind Generation 2 | at 5-minute intervals, last 7 days 3 | Dates: 09Apr2014 - 16Apr2014 (last updated 15Apr2014 11:07:06) Pacific Time 4 | Based on 5-min MW readings from the BPA SCADA system for points 45583, 79687, 79682, 79685 5 | BPA/Technical Operations (TOT-OpInfo@bpa.gov) 6 | 7 | Date/Time Load Wind Hydro Thermal 8 | 04/15/2014 10:10 6553 3732 11225 1599 9 | 04/15/2014 10:15 6580 3686 11230 1603 10 | 04/15/2014 10:20 6560 3700 11254 1602 11 | 04/15/2014 10:25 6537 3684 11281 1601 12 | 04/15/2014 10:30 6562 3680 11260 1607 13 | 04/15/2014 10:35 6525 3675 11212 1608 14 | 04/15/2014 10:40 6496 3706 11240 1605 15 | 04/15/2014 10:45 6514 3700 11261 1607 16 | 04/15/2014 10:50 6501 3727 11172 1607 17 | 04/15/2014 10:55 6451 3700 11066 1596 18 | 04/15/2014 11:00 6449 3696 10816 1601 19 | 04/15/2014 11:05 6464 3688 10662 1601 20 | 04/15/2014 11:10 21 | 04/15/2014 11:15 22 | 04/15/2014 11:20 23 | 04/15/2014 11:25 24 | 04/15/2014 11:30 25 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/20170312_DailyRenewablesWatch.txt: -------------------------------------------------------------------------------- 1 | 03/12/17 Hourly Breakdown of Renewable Resources (MW) 2 | Hour GEOTHERMAL BIOMASS BIOGAS SMALL HYDRO WIND TOTAL SOLAR PV SOLAR THERMAL 3 | 1 935 213 182 417 1590 0 0 4 | 2 935 212 182 413 1355 0 0 5 | 3 "The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. 6 | Parameter name: dateTime" "The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. 7 | Parameter name: dateTime" "The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. 8 | Parameter name: dateTime" "The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. 9 | Parameter name: dateTime" 0 0 "The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. 10 | Parameter name: dateTime" 11 | 4 936 212 180 414 1186 0 0 12 | 5 937 214 178 456 1339 0 0 13 | 6 938 215 170 458 1097 0 0 14 | 7 939 215 170 459 866 0 0 15 | 8 940 217 173 455 851 635 0 16 | 9 930 219 179 423 650 3598 145 17 | 10 876 219 180 389 397 5751 447 18 | 11 789 220 181 399 258 6479 474 19 | 12 889 219 181 400 310 7499 294 20 | 13 937 217 178 388 357 7424 474 21 | 14 935 217 177 387 379 7063 469 22 | 15 934 216 177 389 313 7366 392 23 | 16 932 217 177 390 261 8157 238 24 | 17 931 216 178 453 230 7024 262 25 | 18 931 218 178 457 170 4687 190 26 | 19 933 220 179 474 160 971 27 27 | 20 933 218 182 471 346 0 0 28 | 21 932 216 182 474 593 0 0 29 | 22 933 214 182 506 590 0 0 30 | 23 933 214 182 507 604 0 0 31 | 24 934 213 182 491 573 0 0 32 | 33 | 34 | Hourly Breakdown of Total Production by Resource Type (MW) 35 | Hour RENEWABLES NUCLEAR THERMAL IMPORTS HYDRO 36 | 1 2920 2282 3575 6685 4435 37 | 2 2684 2283 7525 6594 0 38 | 3 0 0 #VALUE! #VALUE! 0 39 | 4 2514 2283 2903 6488 4462 40 | 5 2669 2281 2926 6306 4236 41 | 6 2421 2280 2919 6663 4370 42 | 7 2649 2281 2557 7319 4504 43 | 8 3271 2282 2634 7204 4144 44 | 9 6145 2283 2210 5070 3933 45 | 10 8260 2284 1876 3592 3541 46 | 11 8801 2284 1800 3221 3442 47 | 12 9792 2283 1767 2346 3465 48 | 13 9976 2283 1739 2302 3440 49 | 14 9627 2283 2010 2612 3487 50 | 15 9786 2283 2369 2562 3557 51 | 16 10371 2281 2548 2595 3600 52 | 17 9295 2280 3257 3294 3868 53 | 18 6833 2282 4582 5261 4306 54 | 19 2964 2282 7143 7437 4828 55 | 20 1678 2280 9166 8386 4773 56 | 21 1923 2281 8907 8522 4604 57 | 22 1918 2283 7979 8256 4575 58 | 23 1932 2283 6799 7693 4560 59 | 24 1902 2283 5478 7323 4362 60 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/20171104_DailyRenewablesWatch.txt: -------------------------------------------------------------------------------- 1 | 11/04/17 Hourly Breakdown of Renewable Resources (MW) 2 | Hour GEOTHERMAL BIOMASS BIOGAS SMALL HYDRO WIND TOTAL SOLAR PV SOLAR THERMAL 3 | 1 912 258 218 269 1560 0 0 4 | 2 912 257 220 227 1531 0 0 5 | 3 913 251 221 232 1603 0 0 6 | 4 919 261 220 235 1735 0 0 7 | 5 923 265 220 233 2191 0 0 8 | 6 923 262 220 270 2224 0 0 9 | 7 925 265 220 422 2333 0 0 10 | 8 927 265 221 493 2120 328 0 11 | 9 926 266 221 357 2081 2709 0 12 | 10 926 259 220 266 2238 5040 44 13 | 11 925 261 220 268 2157 6479 83 14 | 12 925 263 219 255 2318 6733 115 15 | 13 924 262 220 246 2421 6456 265 16 | 14 924 261 220 250 2602 6428 320 17 | 15 924 263 221 252 2786 6256 323 18 | 16 926 266 220 249 2690 5255 318 19 | 17 928 265 219 287 2486 3227 209 20 | 18 929 262 219 486 2262 502 6 21 | 19 928 264 219 497 2044 0 0 22 | 20 929 265 218 491 2142 0 0 23 | 21 929 265 218 491 2129 0 0 24 | 22 929 264 218 460 2067 0 0 25 | 23 928 262 218 408 2157 0 0 26 | 24 929 261 219 373 2205 0 0 27 | 28 | 29 | Hourly Breakdown of Total Production by Resource Type (MW) 30 | Hour RENEWABLES NUCLEAR THERMAL IMPORTS HYDRO 31 | 1 3159 2257 7329 6113 2387 32 | 2 3147 2256 6894 6061 2078 33 | 3 3220 2255 6480 5864 2011 34 | 4 3371 2254 6227 5650 2019 35 | 5 3834 2255 6075 5386 1993 36 | 6 3899 2256 5767 5955 2189 37 | 7 4165 2256 5659 6609 2358 38 | 8 4354 2256 5913 6944 2437 39 | 9 6561 2258 6027 5635 1993 40 | 10 8993 2258 5065 4630 1896 41 | 11 10394 2257 4533 3868 1867 42 | 12 10829 2257 4381 3697 1811 43 | 13 10795 2257 4276 3644 1668 44 | 14 11005 2253 4047 3416 1704 45 | 15 11025 2250 4194 3087 1716 46 | 16 9924 2249 4710 3922 1789 47 | 17 7620 2250 5932 5364 2088 48 | 18 4666 2251 7808 7013 2580 49 | 19 3952 2251 9651 7411 2845 50 | 20 4045 2251 8859 7967 3003 51 | 21 4032 2252 8293 8181 2755 52 | 22 3938 2253 7661 8101 2602 53 | 23 3973 2253 7449 6975 2586 54 | 24 3987 2254 6650 6429 2426 55 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/20171105_DailyRenewablesWatch.txt: -------------------------------------------------------------------------------- 1 | 11/05/17 Hourly Breakdown of Renewable Resources (MW) 2 | Hour GEOTHERMAL BIOMASS BIOGAS SMALL HYDRO WIND TOTAL SOLAR PV SOLAR THERMAL 3 | 1 928 264 201 295 2109 0 0 4 | 2 929 264 215 280 2159 0 0 5 | 3 928 263 216 223 2036 0 0 6 | 4 929 263 215 227 2031 0 0 7 | 5 930 264 216 222 2046 0 0 8 | 6 929 265 215 275 2248 0 0 9 | 7 929 262 215 481 2279 342 0 10 | 8 929 258 215 424 2194 2810 12 11 | 9 928 261 215 333 2175 5246 175 12 | 10 926 261 215 256 2281 5833 233 13 | 11 925 262 214 240 2514 6313 243 14 | 12 923 262 219 236 2206 6182 241 15 | 13 922 263 224 238 2227 6328 215 16 | 14 923 261 224 231 2308 6882 291 17 | 15 921 263 217 233 2175 6347 304 18 | 16 921 265 216 240 1850 4035 232 19 | 17 921 267 216 279 1523 626 11 20 | 18 922 266 215 508 1492 0 0 21 | 19 922 274 222 520 1503 0 0 22 | 20 922 253 231 520 1569 0 0 23 | 21 923 212 234 501 1455 0 0 24 | 22 922 225 234 479 1586 0 0 25 | 23 922 230 233 421 1570 0 0 26 | 24 923 237 231 391 1795 0 0 27 | 28 | 29 | Hourly Breakdown of Total Production by Resource Type (MW) 30 | Hour RENEWABLES NUCLEAR THERMAL IMPORTS HYDRO 31 | 1 3599 2254 6462 5560 2205 32 | 2 3847 2256 5817 5111 2141 33 | 3 3666 2254 5690 5120 2148 34 | 4 3665 2253 5266 5510 2078 35 | 5 3678 2254 5358 5497 2125 36 | 6 3932 2257 5292 5467 2466 37 | 7 4508 2258 5386 5444 2357 38 | 8 6842 2258 4743 4755 2012 39 | 9 9333 2260 3972 3813 1703 40 | 10 10006 2261 3676 3478 1708 41 | 11 10711 2259 3436 2866 1660 42 | 12 10269 2260 3635 2971 1757 43 | 13 10417 2258 3923 2537 1578 44 | 14 11120 2257 3772 2115 1699 45 | 15 10460 2257 4428 2421 1762 46 | 16 7759 2259 5976 4143 2063 47 | 17 3843 2260 8878 5994 2739 48 | 18 3403 2261 10803 6904 2818 49 | 19 3442 2262 10582 7344 2968 50 | 20 3495 2263 9737 7566 3019 51 | 21 3325 2262 9275 7712 2766 52 | 22 3446 2262 8332 7406 2455 53 | 23 3376 2260 7739 6354 2468 54 | 24 3578 2260 8570 4369 1960 55 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/20171106_DailyRenewablesWatch.txt: -------------------------------------------------------------------------------- 1 | 11/06/17 Hourly Breakdown of Renewable Resources (MW) 2 | Hour GEOTHERMAL BIOMASS BIOGAS SMALL HYDRO WIND TOTAL SOLAR PV SOLAR THERMAL 3 | 1 923 268 230 265 1660 0 0 4 | 2 923 278 230 223 2186 0 0 5 | 3 924 288 231 182 1836 0 0 6 | 4 926 294 233 217 1996 0 0 7 | 5 926 295 233 254 1642 0 0 8 | 6 926 290 232 414 1461 0 0 9 | 7 926 296 229 432 1358 193 0 10 | 8 926 297 224 416 1327 1775 0 11 | 9 925 293 220 260 1036 4044 0 12 | 10 924 294 225 246 863 6217 0 13 | 11 922 297 226 218 1064 6505 32 14 | 12 919 299 227 210 1117 6506 54 15 | 13 922 299 228 214 1344 6458 83 16 | 14 921 300 228 221 1489 5788 266 17 | 15 921 303 228 241 1522 3880 279 18 | 16 921 301 227 271 1401 2004 60 19 | 17 921 299 228 456 1413 178 0 20 | 18 923 296 229 508 1277 0 0 21 | 19 924 298 227 507 1255 0 0 22 | 20 924 298 229 472 1261 0 0 23 | 21 924 296 226 465 1057 0 0 24 | 22 925 298 224 469 1004 0 0 25 | 23 926 287 226 429 1131 0 0 26 | 24 924 256 227 371 1285 0 0 27 | 28 | 29 | Hourly Breakdown of Total Production by Resource Type (MW) 30 | Hour RENEWABLES NUCLEAR THERMAL IMPORTS HYDRO 31 | 1 3346 2257 7874 4579 1720 32 | 2 3840 2257 6909 4404 1810 33 | 3 3461 2258 6899 4572 1817 34 | 4 3666 2258 6855 4468 1777 35 | 5 3350 2261 7562 4520 2122 36 | 6 3323 2264 8408 5164 2312 37 | 7 3434 2264 9522 6242 2337 38 | 8 4965 2264 9642 6078 1921 39 | 9 6778 2264 8725 5449 1670 40 | 10 8769 2261 7247 4461 1668 41 | 11 9264 2258 7156 4074 1655 42 | 12 9332 2257 7676 4004 1585 43 | 13 9548 2255 7635 3879 1550 44 | 14 9213 2255 8022 3933 1772 45 | 15 7374 2257 9528 4262 2066 46 | 16 5185 2259 11525 5035 2235 47 | 17 3495 2261 13102 5796 2480 48 | 18 3233 2260 13987 6769 2758 49 | 19 3211 2261 13780 7177 2704 50 | 20 3184 2259 12966 7409 2661 51 | 21 2968 2259 12853 6947 2389 52 | 22 2920 2260 11944 6427 2295 53 | 23 2999 2260 11152 5187 2180 54 | 24 3063 2259 10221 4561 1969 55 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/ene_slrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2014-02-24T20:25:40-00:00 5 | OASIS 6 | v20131201 7 | 8 | 9 | 10 | CAISO 11 | 12 | 13 | OASIS 14 | PPT 15 | ENE_SLRS 16 | DAM 17 | MW 18 | ENDING 19 | 3600 20 | 21 | 22 | ISO_TOT_EXP_MW 23 | Caiso_Totals 24 | 2013-09-19 25 | 9 26 | 2013-09-19T15:00:00-00:00 27 | 2013-09-19T16:00:00-00:00 28 | 704 29 | 30 | 31 | ISO_TOT_EXP_MW 32 | Caiso_Totals 33 | 2013-09-19 34 | 14 35 | 2013-09-19T20:00:00-00:00 36 | 2013-09-19T21:00:00-00:00 37 | 884 38 | 39 | 40 | ISO_TOT_GEN_MW 41 | Caiso_Totals 42 | 2013-09-19 43 | 18 44 | 2013-09-20T00:00:00-00:00 45 | 2013-09-20T01:00:00-00:00 46 | 27521.96 47 | 48 | 49 | ISO_TOT_GEN_MW 50 | Caiso_Totals 51 | 2013-09-19 52 | 11 53 | 2013-09-19T17:00:00-00:00 54 | 2013-09-19T18:00:00-00:00 55 | 23900.79 56 | 57 | 58 | ISO_TOT_IMP_MW 59 | Caiso_Totals 60 | 2013-09-19 61 | 14 62 | 2013-09-19T20:00:00-00:00 63 | 2013-09-19T21:00:00-00:00 64 | 7248 65 | 66 | 67 | ISO_TOT_IMP_MW 68 | Caiso_Totals 69 | 2013-09-19 70 | 1 71 | 2013-09-19T07:00:00-00:00 72 | 2013-09-19T08:00:00-00:00 73 | 5014 74 | 75 | 76 | The contents of these pages are subject to change without notice. Decisions based on information contained within the California ISO's web site are the visitor's sole responsibility. 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/ren_report.csv: -------------------------------------------------------------------------------- 1 | 03/12/14 Hourly Breakdown of Renewable Resources (MW) 2 | Hour GEOTHERMAL BIOMASS BIOGAS SMALL HYDRO WIND TOTAL SOLAR PV SOLAR THERMAL 3 | 1 900 313 190 170 1596 0 0 4 | 2 900 314 189 169 1814 0 0 5 | 3 900 328 190 170 2076 0 0 6 | 4 900 334 190 167 2086 0 0 7 | 5 900 344 190 167 1893 0 0 8 | 6 900 344 190 166 1650 0 0 9 | 7 899 339 189 177 1459 0 0 10 | 8 897 341 188 179 1487 245 0 11 | 9 898 341 187 171 1502 1455 5 12 | 10 898 341 186 169 1745 2613 251 13 | 11 898 342 183 168 2204 3318 336 14 | 12 897 358 185 168 2241 3558 341 15 | 13 897 367 189 169 1850 3585 326 16 | 14 897 367 191 169 1632 3598 316 17 | 15 892 364 193 171 1317 3541 299 18 | 16 890 368 192 174 1156 3359 338 19 | 17 891 367 189 187 1082 2697 400 20 | 18 892 362 192 202 1047 1625 325 21 | 19 892 361 196 205 861 306 32 22 | 20 892 359 197 214 725 0 0 23 | 21 895 354 192 208 722 0 0 24 | 22 900 355 182 209 689 0 0 25 | 23 901 355 182 198 482 0 0 26 | 24 902 357 183 170 507 0 0 27 | 28 | 29 | Hourly Breakdown of Total Production by Resource Type (MW) 30 | Hour RENEWABLES NUCLEAR THERMAL IMPORTS HYDRO 31 | 1 3170 1092 6862 9392 871 32 | 2 3386 1092 6181 9144 709 33 | 3 3663 1092 6016 8993 675 34 | 4 3678 1091 5900 9107 669 35 | 5 3494 1092 6041 9080 840 36 | 6 3251 1091 7115 8848 919 37 | 7 3064 1091 9917 8464 1269 38 | 8 3337 1091 10955 8694 1387 39 | 9 4559 1091 10291 8667 1120 40 | 10 6204 1091 8967 8783 829 41 | 11 7449 1091 8095 8776 656 42 | 12 7747 1089 8068 8565 703 43 | 13 7382 1087 8157 8736 862 44 | 14 7171 1085 8564 8633 973 45 | 15 6776 1079 8588 8832 1004 46 | 16 6477 1079 8890 8857 980 47 | 17 5812 1080 8937 9168 1210 48 | 18 4645 1081 9620 9481 1443 49 | 19 2853 1081 11258 9653 1763 50 | 20 2388 1081 12971 10022 1979 51 | 21 2370 1081 12644 10324 2130 52 | 22 2335 1082 11820 10069 1774 53 | 23 2119 1085 10712 9871 1250 54 | 24 2118 1082 9800 8904 935 -------------------------------------------------------------------------------- /tests/fixtures/caiso/sld_forecast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2014-05-09T17:50:06-00:00 5 | OASIS 6 | v20140401 7 | 8 | 9 | 10 | CAISO 11 | 12 | 13 | OASIS 14 | PPT 15 | SLD_FCST 16 | RTM 17 | RTPD 18 | MW 19 | ENDING 20 | 900 21 | 22 | 23 | SYS_FCST_15MIN_MW 24 | CA ISO-TAC 25 | 2014-05-08 26 | 50 27 | 2014-05-08T19:15:00-00:00 28 | 2014-05-08T19:30:00-00:00 29 | 26723 30 | 31 | 32 | SYS_FCST_5MIN_MW 33 | CA ISO-TAC 34 | 2014-05-08 35 | 144 36 | 2014-05-08T18:55:00-00:00 37 | 2014-05-08T19:00:00-00:00 38 | 26755 39 | 40 | 41 | SYS_FCST_5MIN_MW 42 | PGE-TAC 43 | 2014-05-08 44 | 146 45 | 2014-05-08T19:05:00-00:00 46 | 2014-05-08T19:10:00-00:00 47 | 11530 48 | 49 | 50 | 51 | The contents of these pages are subject to change without notice. Decisions based on information contained within the California ISO's web site are the visitor's sole responsibility. 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/sld_ren_forecast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2014-02-24T18:51:45-00:00 5 | OASIS 6 | v20131201 7 | 8 | 9 | 10 | CAISO 11 | 12 | 13 | OASIS 14 | PPT 15 | SLD_REN_FCST 16 | DAM 17 | MW 18 | ENDING 19 | 3600 20 | 21 | 22 | RENEW_FCST_DA_MW 23 | 2013-09-19 24 | 1 25 | 2013-09-19T07:00:00-00:00 26 | 2013-09-19T08:00:00-00:00 27 | 0.01 28 | NP15 29 | Solar 30 | 31 | 32 | RENEW_FCST_DA_MW 33 | 2013-09-19 34 | 2 35 | 2013-09-19T08:00:00-00:00 36 | 2013-09-19T09:00:00-00:00 37 | 0 38 | NP15 39 | Solar 40 | 41 | 42 | 43 | 44 | OASIS 45 | PPT 46 | SLD_REN_FCST 47 | DAM 48 | MW 49 | ENDING 50 | 3600 51 | 52 | 53 | RENEW_FCST_DA_MW 54 | 2013-09-19 55 | 1 56 | 2013-09-19T07:00:00-00:00 57 | 2013-09-19T08:00:00-00:00 58 | 478.86 59 | NP15 60 | Wind 61 | 62 | 63 | RENEW_FCST_DA_MW 64 | 2013-09-19 65 | 24 66 | 2013-09-20T06:00:00-00:00 67 | 2013-09-20T07:00:00-00:00 68 | 580.83 69 | NP15 70 | Wind 71 | 72 | 73 | 74 | The contents of these pages are subject to change without notice. Decisions based on information contained within the California ISO's web site are the visitor's sole responsibility. 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/systemconditions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | System Conditions - The California ISO 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 71 | 73 | 74 |
18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 68 |
Current System Demand:
20 | (Actual Demand at this point in time)
21 | 25224 MW
 
Today's Peak Demand:
30 | (Highest point thus far today)
31 | 26493 MW
 
Today's Forecast Peak Demand:
40 | (Highest point expected today. Does not appear post-peak.)
41 | 30170 MW
 
Tomorrow's Forecast Peak Demand:
50 | (Not included on graph)
51 | 29295 MW 52 |
   
Information 63 | is current as of 64 | 05-Jan-2017 13:50 65 | . If browser does not support auto refresh, select reload. 66 |

The ISO markets procure the resources necessary to meet 100% of the demand forecast plus the capacity necessary to maintain the required regulating and contingency operating reserves.
69 | 70 |
72 |
75 |
76 | 77 | -------------------------------------------------------------------------------- /tests/fixtures/caiso/todays_outlook_renewables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | System Conditions - The California ISO 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 |

Current Renewables
16 | 17 | 6086 18 | MW
19 |

20 |


21 | Renewables Watch
22 | The Renewables Watch provides actual renewable energy production within the ISO Grid.
23 | Click here to view yesterday's output.



26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/fixtures/ercot/real_time_system_conditions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Real-Time System Conditions 7 | 8 | 9 | 14 | 15 | 24 | 25 | 26 | 27 |
28 |
29 |
Real-Time System Conditions
30 | 31 |
32 |
33 |
Last Updated: Apr 14, 2016 18:38:40
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 |
Frequency
Current Frequency59.998
Instantaneous Time Error-17.468
Real-Time Data
Actual System Demand38850
Total System Capacity (not including Ancillary Services)42514
Total Wind Output5242
DC Tie Flows
DC_E (East)-31
DC_L (Laredo VFT)0
DC_N (North)0
DC_R (Railroad)0
DC_S (Eagle Pass)1
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /tests/fixtures/eu/de_load.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | b280483d47e943bd85f2da2637e013b6 4 | 1 5 | A65 6 | A16 7 | 10X1001A1001A450 8 | A32 9 | 10X1001A1001A450 10 | A33 11 | 2017-10-30T03:11:53Z 12 | 13 | 2017-10-29T20:00Z 14 | 2017-10-30T00:00Z 15 | 16 | 17 | 1 18 | A04 19 | A01 20 | 10YDE-EON------1 21 | MAW 22 | A01 23 | 24 | 25 | 26 | 2017-10-29T20:00Z 27 | 2017-10-30T00:00Z 28 | 29 | PT15M 30 | 31 | 1 32 | 16698 33 | 34 | 35 | 2 36 | 16489 37 | 38 | 39 | 3 40 | 16276 41 | 42 | 43 | 4 44 | 16219 45 | 46 | 47 | 5 48 | 16875 49 | 50 | 51 | 6 52 | 16267 53 | 54 | 55 | 7 56 | 16009 57 | 58 | 59 | 8 60 | 15694 61 | 62 | 63 | 9 64 | 15205 65 | 66 | 67 | 10 68 | 14890 69 | 70 | 71 | 11 72 | 14575 73 | 74 | 75 | 12 76 | 14485 77 | 78 | 79 | 13 80 | 14251 81 | 82 | 83 | 14 84 | 13820 85 | 86 | 87 | 15 88 | 13949 89 | 90 | 91 | 16 92 | 13926 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /tests/fixtures/isone/morningreport_20160101.json: -------------------------------------------------------------------------------- 1 | { 2 | "MorningReports": { 3 | "MorningReport": [ 4 | { 5 | "AddlCapAvailOp4ActMw": 0, 6 | "AmsPeakLoadExpMw": 20322, 7 | "BeginDate": "2016-01-01T00:00:00.000-05:00", 8 | "CapAdditionsMw": 2189, 9 | "CapRequiredMw": 17998, 10 | "CityForecastDetail": [ 11 | { 12 | "CityName": "Boston", 13 | "HighTemperature": 36, 14 | "WeatherConditions": "Partly Cloudy", 15 | "WindDirSpeed": "W-10" 16 | }, 17 | { 18 | "CityName": "Hartford", 19 | "HighTemperature": 35, 20 | "WeatherConditions": "Mostly Cloudy", 21 | "WindDirSpeed": "W-8" 22 | } 23 | ], 24 | "CreationDate": "2016-01-01T08:14:51.097-05:00", 25 | "CsoMw": 30389, 26 | "ExcessCommitMw": 1828, 27 | "ExpActOp4Mw": 0, 28 | "GenOutagesReductionMW": 1227, 29 | "GeoMagDistIsoAction": "None", 30 | "GeoMagDistOthCntrAction": "None", 31 | "InterchangeDetail": [ 32 | { 33 | "ExportLimitOutMw": 0, 34 | "ImportLimitInMw": -218, 35 | "ScheduledMw": -218, 36 | "TieName": "Highgate" 37 | }, 38 | { 39 | "ExportLimitOutMw": 200, 40 | "ImportLimitInMw": -1000, 41 | "ScheduledMw": -741, 42 | "TieName": "NB" 43 | }, 44 | { 45 | "ExportLimitOutMw": 1200, 46 | "ImportLimitInMw": -1400, 47 | "ScheduledMw": -1400, 48 | "TieName": "NYISO AC" 49 | }, 50 | { 51 | "ExportLimitOutMw": 330, 52 | "ImportLimitInMw": -346, 53 | "ScheduledMw": 0, 54 | "TieName": "NYISO CSC" 55 | }, 56 | { 57 | "ExportLimitOutMw": 200, 58 | "ImportLimitInMw": -200, 59 | "ScheduledMw": -200, 60 | "TieName": "NYISO NNC" 61 | }, 62 | { 63 | "ExportLimitOutMw": 1200, 64 | "ImportLimitInMw": -2000, 65 | "ScheduledMw": -1337, 66 | "TieName": "Phase 2" 67 | } 68 | ], 69 | "LargestFirstContMw": 1337, 70 | "NetCapDeliveryMw": -3896, 71 | "NonCommUnitsCapMw": 0, 72 | "NyIsoSarAvail": "Y", 73 | "PeakLoadTodayHour": "2016-01-01T18:00:00.000-05:00", 74 | "PeakLoadTodayMw": 15770, 75 | "PeakLoadYesterdayHour": "2015-12-31T18:00:00.000-05:00", 76 | "PeakLoadYesterdayMw": 16498, 77 | "ReplReserveRequiredMw": 180, 78 | "ReportType": "Final", 79 | "SurplusDeficiencyMw": 2008, 80 | "TenMinReserveEstMw": 1604, 81 | "TenMinReserveReqMw": 1604, 82 | "ThirtyMinReserveEstMw": 2632, 83 | "ThirtyMinReserveReqMw": 624, 84 | "TieDelivery": [ 85 | { 86 | "TieFlowMw": -218, 87 | "TieName": "Highgate" 88 | }, 89 | { 90 | "TieFlowMw": -741, 91 | "TieName": "NB" 92 | }, 93 | { 94 | "TieFlowMw": -1400, 95 | "TieName": "NYISO AC" 96 | }, 97 | { 98 | "TieFlowMw": 0, 99 | "TieName": "NYISO CSC" 100 | }, 101 | { 102 | "TieFlowMw": -200, 103 | "TieName": "NYISO NNC" 104 | }, 105 | { 106 | "TieFlowMw": -1337, 107 | "TieName": "Phase 2" 108 | } 109 | ], 110 | "TotAvailCapMw": 20006, 111 | "TotalOperReserveReqMw": 2228, 112 | "UncommittedAvailGenMw": 15241, 113 | "UnitCommMinOrrCount": 0, 114 | "UnitCommMinOrrMw": 0 115 | } 116 | ] 117 | } 118 | } -------------------------------------------------------------------------------- /tests/fixtures/isone/morningreport_current.json: -------------------------------------------------------------------------------- 1 | { 2 | "MorningReports": { 3 | "MorningReport": [ 4 | { 5 | "AddlCapAvailOp4ActMw": 0, 6 | "AmsPeakLoadExpMw": 21110, 7 | "BeginDate": "2017-01-30T00:00:00.000-05:00", 8 | "CapAdditionsMw": 2027, 9 | "CapRequiredMw": 20242, 10 | "CityForecastDetail": [ 11 | { 12 | "CityName": "Boston", 13 | "HighTemperature": 31, 14 | "WeatherConditions": "Moslty Cloudy", 15 | "WindDirSpeed": "NW-13" 16 | }, 17 | { 18 | "CityName": "Hartford", 19 | "HighTemperature": 26, 20 | "WeatherConditions": "Mostly Clear", 21 | "WindDirSpeed": "NW-10" 22 | } 23 | ], 24 | "CreationDate": "2017-01-30T08:14:51.392-05:00", 25 | "CsoMw": 30280, 26 | "ExcessCommitMw": 1452, 27 | "ExpActOp4Mw": 0, 28 | "GenOutagesReductionMW": 2426, 29 | "GeoMagDistIsoAction": "None", 30 | "GeoMagDistOthCntrAction": "None", 31 | "InterchangeDetail": [ 32 | { 33 | "ExportLimitOutMw": 0, 34 | "ImportLimitInMw": -225, 35 | "ScheduledMw": -205, 36 | "TieName": "Highgate" 37 | }, 38 | { 39 | "ExportLimitOutMw": 200, 40 | "ImportLimitInMw": -1000, 41 | "ScheduledMw": -490, 42 | "TieName": "NB" 43 | }, 44 | { 45 | "ExportLimitOutMw": 1200, 46 | "ImportLimitInMw": -1400, 47 | "ScheduledMw": -981, 48 | "TieName": "NYISO AC" 49 | }, 50 | { 51 | "ExportLimitOutMw": 330, 52 | "ImportLimitInMw": -346, 53 | "ScheduledMw": 215, 54 | "TieName": "NYISO CSC" 55 | }, 56 | { 57 | "ExportLimitOutMw": 200, 58 | "ImportLimitInMw": -200, 59 | "ScheduledMw": -30, 60 | "TieName": "NYISO NNC" 61 | }, 62 | { 63 | "ExportLimitOutMw": 1200, 64 | "ImportLimitInMw": -2000, 65 | "ScheduledMw": -1371, 66 | "TieName": "Phase 2" 67 | } 68 | ], 69 | "LargestFirstContMw": 1630, 70 | "NetCapDeliveryMw": -2862, 71 | "NonCommUnitsCapMw": 0, 72 | "NyIsoSarAvail": "Y", 73 | "PeakLoadTodayHour": "2017-01-30T19:00:00.000-05:00", 74 | "PeakLoadTodayMw": 17600, 75 | "PeakLoadYesterdayHour": "2017-01-29T19:00:00.000-05:00", 76 | "PeakLoadYesterdayMw": 16074, 77 | "ReplReserveRequiredMw": 180, 78 | "ReportType": "Final", 79 | "SurplusDeficiencyMw": 1632, 80 | "TenMinReserveEstMw": 1956, 81 | "TenMinReserveReqMw": 1956, 82 | "ThirtyMinReserveEstMw": 2318, 83 | "ThirtyMinReserveReqMw": 686, 84 | "TieDelivery": [ 85 | { 86 | "TieFlowMw": -205, 87 | "TieName": "Highgate" 88 | }, 89 | { 90 | "TieFlowMw": -490, 91 | "TieName": "NB" 92 | }, 93 | { 94 | "TieFlowMw": -981, 95 | "TieName": "NYISO AC" 96 | }, 97 | { 98 | "TieFlowMw": 215, 99 | "TieName": "NYISO CSC" 100 | }, 101 | { 102 | "TieFlowMw": -30, 103 | "TieName": "NYISO NNC" 104 | }, 105 | { 106 | "TieFlowMw": -1371, 107 | "TieName": "Phase 2" 108 | } 109 | ], 110 | "TotAvailCapMw": 21874, 111 | "TotalOperReserveReqMw": 2642, 112 | "UncommittedAvailGenMw": 10869, 113 | "UnitCommMinOrrCount": 0, 114 | "UnitCommMinOrrMw": 0 115 | } 116 | ] 117 | } 118 | } -------------------------------------------------------------------------------- /tests/fixtures/nbpower/2017-03-12 00.csv: -------------------------------------------------------------------------------- 1 | 20170312000000AS,2020 2 | 20170312010000AS,1982 3 | 20170312030000AD,1948 4 | 20170312030000AD,1974 5 | -------------------------------------------------------------------------------- /tests/fixtures/nbpower/2017-03-13 00.csv: -------------------------------------------------------------------------------- 1 | 20170313000000AD,2070 2 | 20170313010000AD,2065 3 | 20170313020000AD,2090 4 | 20170313030000AD,2130 5 | -------------------------------------------------------------------------------- /tests/fixtures/nbpower/2017-07-16 22.csv: -------------------------------------------------------------------------------- 1 | 20170716220000AD,1263,0 2 | 20170716230000AD,1160,0 3 | 20170717000000AD,1129,0 4 | 20170717010000AD,1089,0 -------------------------------------------------------------------------------- /tests/fixtures/nbpower/2017-11-05 00.csv: -------------------------------------------------------------------------------- 1 | 20171105000000AD,1293,0 2 | 20171105010000AD,1266,0 3 | 20171105010000AS,1261,0 4 | 20171105030000AS,1262,0 5 | -------------------------------------------------------------------------------- /tests/fixtures/nbpower/2017-11-06 00.csv: -------------------------------------------------------------------------------- 1 | 20171106000000AS,1196,0 2 | 20171106010000AS,1183,0 3 | 20171106020000AS,1182,0 4 | 20171106030000AS,1064,0 5 | -------------------------------------------------------------------------------- /tests/fixtures/nbpower/SystemInformation_realtime.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | NB Power 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
System Information
 
27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 83 | 84 |
  Net Scheduled Interchange Reserve Margins 
NB Load NB Demand ISO-NE EMEC MPS QUEBEC NOVA SCOTIA PEI 10 Min Reserve Margin 10 Min Spinning Reserve Margin 30 Min Reserve Margin 
1150120072911-9-74145148308393263
70 | Note: For actual and scheduled columns, positive values are exports and negative values are imports. 71 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
Transmission Outages are now available from the Data page
82 |
85 | Jul 16, 2017 22:57:29 Atlantic Time. Times at which values are sampled may vary by as much as 5 minutes.

86 | This page will automatically refresh itself every 5 minutes. 87 |
88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /tests/fixtures/nspower/currentload.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "datetime": "\/Date(1507114800000)\/", 4 | "Base Load": 0 5 | }, 6 | { 7 | "datetime": "\/Date(1507118400000)\/", 8 | "Base Load": 986.93 9 | }, 10 | { 11 | "datetime": "\/Date(1507122000000)\/", 12 | "Base Load": 995.22 13 | }, 14 | { 15 | "datetime": "\/Date(1507125600000)\/", 16 | "Base Load": 998.03 17 | }, 18 | { 19 | "datetime": "\/Date(1507129200000)\/", 20 | "Base Load": 988.01 21 | }, 22 | { 23 | "datetime": "\/Date(1507132800000)\/", 24 | "Base Load": 974.39 25 | }, 26 | { 27 | "datetime": "\/Date(1507136400000)\/", 28 | "Base Load": 957.96 29 | }, 30 | { 31 | "datetime": "\/Date(1507140000000)\/", 32 | "Base Load": 953.6 33 | }, 34 | { 35 | "datetime": "\/Date(1507143600000)\/", 36 | "Base Load": 971.28 37 | }, 38 | { 39 | "datetime": "\/Date(1507147200000)\/", 40 | "Base Load": 982.74 41 | }, 42 | { 43 | "datetime": "\/Date(1507150800000)\/", 44 | "Base Load": 973.65 45 | }, 46 | { 47 | "datetime": "\/Date(1507154400000)\/", 48 | "Base Load": 997.68 49 | }, 50 | { 51 | "datetime": "\/Date(1507158000000)\/", 52 | "Base Load": 949.77 53 | }, 54 | { 55 | "datetime": "\/Date(1507161600000)\/", 56 | "Base Load": 877.25 57 | }, 58 | { 59 | "datetime": "\/Date(1507165200000)\/", 60 | "Base Load": 792.89 61 | }, 62 | { 63 | "datetime": "\/Date(1507168800000)\/", 64 | "Base Load": 720.62 65 | }, 66 | { 67 | "datetime": "\/Date(1507172400000)\/", 68 | "Base Load": 647.86 69 | }, 70 | { 71 | "datetime": "\/Date(1507176000000)\/", 72 | "Base Load": 613.36 73 | }, 74 | { 75 | "datetime": "\/Date(1507179600000)\/", 76 | "Base Load": 588.92 77 | }, 78 | { 79 | "datetime": "\/Date(1507183200000)\/", 80 | "Base Load": 575.8 81 | }, 82 | { 83 | "datetime": "\/Date(1507186800000)\/", 84 | "Base Load": 582.15 85 | }, 86 | { 87 | "datetime": "\/Date(1507190400000)\/", 88 | "Base Load": 627.27 89 | }, 90 | { 91 | "datetime": "\/Date(1507194000000)\/", 92 | "Base Load": 751.71 93 | }, 94 | { 95 | "datetime": "\/Date(1507197600000)\/", 96 | "Base Load": 863.55 97 | }, 98 | { 99 | "datetime": "\/Date(1507201200000)\/", 100 | "Base Load": 892.64 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /tests/fixtures/nspower/forecast.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "datetime": "\/Date(1507201200000)\/", 4 | "Current Forecasted Demand": 973 5 | }, 6 | { 7 | "datetime": "\/Date(1507114800000)\/", 8 | "Current Forecasted Demand": 1088 9 | }, 10 | { 11 | "datetime": "\/Date(1507204800000)\/", 12 | "Current Forecasted Demand": 1016 13 | }, 14 | { 15 | "datetime": "\/Date(1507118400000)\/", 16 | "Current Forecasted Demand": 1113 17 | }, 18 | { 19 | "datetime": "\/Date(1507208400000)\/", 20 | "Current Forecasted Demand": 1047 21 | }, 22 | { 23 | "datetime": "\/Date(1507122000000)\/", 24 | "Current Forecasted Demand": 1105 25 | }, 26 | { 27 | "datetime": "\/Date(1507212000000)\/", 28 | "Current Forecasted Demand": 971 29 | }, 30 | { 31 | "datetime": "\/Date(1507125600000)\/", 32 | "Current Forecasted Demand": 1105 33 | }, 34 | { 35 | "datetime": "\/Date(1507215600000)\/", 36 | "Current Forecasted Demand": 991 37 | }, 38 | { 39 | "datetime": "\/Date(1507129200000)\/", 40 | "Current Forecasted Demand": 1092 41 | }, 42 | { 43 | "datetime": "\/Date(1507219200000)\/", 44 | "Current Forecasted Demand": 990 45 | }, 46 | { 47 | "datetime": "\/Date(1507132800000)\/", 48 | "Current Forecasted Demand": 1080 49 | }, 50 | { 51 | "datetime": "\/Date(1507222800000)\/", 52 | "Current Forecasted Demand": 989 53 | }, 54 | { 55 | "datetime": "\/Date(1507136400000)\/", 56 | "Current Forecasted Demand": 1070 57 | }, 58 | { 59 | "datetime": "\/Date(1507226400000)\/", 60 | "Current Forecasted Demand": 984 61 | }, 62 | { 63 | "datetime": "\/Date(1507140000000)\/", 64 | "Current Forecasted Demand": 1056 65 | }, 66 | { 67 | "datetime": "\/Date(1507230000000)\/", 68 | "Current Forecasted Demand": 993 69 | }, 70 | { 71 | "datetime": "\/Date(1507143600000)\/", 72 | "Current Forecasted Demand": 1009 73 | }, 74 | { 75 | "datetime": "\/Date(1507233600000)\/", 76 | "Current Forecasted Demand": 1018 77 | }, 78 | { 79 | "datetime": "\/Date(1507147200000)\/", 80 | "Current Forecasted Demand": 1074.01 81 | }, 82 | { 83 | "datetime": "\/Date(1507237200000)\/", 84 | "Current Forecasted Demand": 1022 85 | }, 86 | { 87 | "datetime": "\/Date(1507150800000)\/", 88 | "Current Forecasted Demand": 1050.01 89 | }, 90 | { 91 | "datetime": "\/Date(1507240800000)\/", 92 | "Current Forecasted Demand": 1002 93 | }, 94 | { 95 | "datetime": "\/Date(1507154400000)\/", 96 | "Current Forecasted Demand": 1079 97 | }, 98 | { 99 | "datetime": "\/Date(1507244400000)\/", 100 | "Current Forecasted Demand": 1020 101 | }, 102 | { 103 | "datetime": "\/Date(1507158000000)\/", 104 | "Current Forecasted Demand": 1087 105 | }, 106 | { 107 | "datetime": "\/Date(1507248000000)\/", 108 | "Current Forecasted Demand": 982 109 | }, 110 | { 111 | "datetime": "\/Date(1507161600000)\/", 112 | "Current Forecasted Demand": 1054 113 | }, 114 | { 115 | "datetime": "\/Date(1507251600000)\/", 116 | "Current Forecasted Demand": 917 117 | }, 118 | { 119 | "datetime": "\/Date(1507165200000)\/", 120 | "Current Forecasted Demand": 971 121 | }, 122 | { 123 | "datetime": "\/Date(1507255200000)\/", 124 | "Current Forecasted Demand": 834 125 | }, 126 | { 127 | "datetime": "\/Date(1507168800000)\/", 128 | "Current Forecasted Demand": 902 129 | }, 130 | { 131 | "datetime": "\/Date(1507258800000)\/", 132 | "Current Forecasted Demand": 759 133 | }, 134 | { 135 | "datetime": "\/Date(1507172400000)\/", 136 | "Current Forecasted Demand": 878 137 | }, 138 | { 139 | "datetime": "\/Date(1507262400000)\/", 140 | "Current Forecasted Demand": 714 141 | }, 142 | { 143 | "datetime": "\/Date(1507176000000)\/", 144 | "Current Forecasted Demand": 827 145 | }, 146 | { 147 | "datetime": "\/Date(1507266000000)\/", 148 | "Current Forecasted Demand": 680 149 | }, 150 | { 151 | "datetime": "\/Date(1507179600000)\/", 152 | "Current Forecasted Demand": 841 153 | }, 154 | { 155 | "datetime": "\/Date(1507269600000)\/", 156 | "Current Forecasted Demand": 660 157 | }, 158 | { 159 | "datetime": "\/Date(1507183200000)\/", 160 | "Current Forecasted Demand": 814.99 161 | }, 162 | { 163 | "datetime": "\/Date(1507273200000)\/", 164 | "Current Forecasted Demand": 650 165 | }, 166 | { 167 | "datetime": "\/Date(1507186800000)\/", 168 | "Current Forecasted Demand": 814 169 | }, 170 | { 171 | "datetime": "\/Date(1507276800000)\/", 172 | "Current Forecasted Demand": 655 173 | }, 174 | { 175 | "datetime": "\/Date(1507190400000)\/", 176 | "Current Forecasted Demand": 797 177 | }, 178 | { 179 | "datetime": "\/Date(1507280400000)\/", 180 | "Current Forecasted Demand": 691 181 | }, 182 | { 183 | "datetime": "\/Date(1507194000000)\/", 184 | "Current Forecasted Demand": 786 185 | }, 186 | { 187 | "datetime": "\/Date(1507284000000)\/", 188 | "Current Forecasted Demand": 809 189 | }, 190 | { 191 | "datetime": "\/Date(1507197600000)\/", 192 | "Current Forecasted Demand": 853 193 | }, 194 | { 195 | "datetime": "\/Date(1507287600000)\/", 196 | "Current Forecasted Demand": 927 197 | }, 198 | { 199 | "datetime": "\/Date(1507201200000)\/", 200 | "Current Forecasted Demand": 973 201 | } 202 | ] 203 | -------------------------------------------------------------------------------- /tests/fixtures/nvenergy/tomorrow.htm: -------------------------------------------------------------------------------- 1 | NV ENERGY - TOMORROW'S LOAD FORECAST 2 | 3 |

NV ENERGY - TOMORROW'S LOAD FORECAST

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
For 12-25-2017 Hour Ending
NAMEDATE12345678910111213141516171819202122232425TOTAL
Forecast Native LoadTOTAL2876 2826 2744 2757 2776 2839 2904 2881 2872 2836 2857 2812 2781 2748 2757 2827 3059 3311 3223 3207 3203 3130 3054 2927 0 70207
Forecast System LoadTOTAL3422 3373 3293 3305 3324 3385 3449 3429 3421 3386 3406 3362 3331 3299 3307 3375 3599 3844 3759 3743 3740 3669 3594 3471 0 83286
60 |

Updated 12-24-2017 09:17:29

61 | NOTE: 62 | Load Forecasts are generated from forecast tools that use a regression model with the following underlying assumptions:
63 | historic actual system-wide loads (SYSTEM) and historic actual hourly native load,
64 | historic actual and forecasted hourly weather data.
65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/fixtures/nyiso/lmp.csv: -------------------------------------------------------------------------------- 1 | 2 | "Time Stamp","Name","PTID","LBMP ($/MWHr)","Marginal Cost Losses ($/MWHr)","Marginal Cost Congestion ($/MWHr)" 3 | "02/18/2016 00:15:00","CAPITL",61757,21.53,1.69,0.00 4 | "02/18/2016 00:15:00","CENTRL",61754,20.70,0.85,0.00 5 | "02/18/2016 00:15:00","DUNWOD",61760,21.73,1.89,0.00 6 | "02/18/2016 00:15:00","GENESE",61753,20.46,0.62,0.00 7 | "02/18/2016 00:15:00","H Q",61844,19.21,-0.64,0.00 8 | "02/18/2016 00:15:00","HUD VL",61758,21.73,1.89,0.00 9 | "02/18/2016 00:15:00","LONGIL",61762,21.97,2.12,0.00 10 | "02/18/2016 00:15:00","MHK VL",61756,20.86,1.01,0.00 11 | "02/18/2016 00:15:00","MILLWD",61759,21.77,1.92,0.00 12 | "02/18/2016 00:15:00","N.Y.C.",61761,21.85,2.00,0.00 13 | "02/18/2016 00:15:00","NORTH",61755,18.69,-1.15,0.00 14 | "02/18/2016 00:15:00","NPX",61845,21.55,1.71,0.00 15 | "02/18/2016 00:15:00","O H",61846,20.30,0.46,0.00 16 | "02/18/2016 00:15:00","PJM",61847,21.13,1.29,0.00 17 | "02/18/2016 00:15:00","WEST",61752,20.74,0.89,0.00 18 | "02/18/2016 00:30:00","CAPITL",61757,21.42,1.68,0.00 19 | "02/18/2016 00:30:00","CENTRL",61754,20.57,0.83,0.00 20 | "02/18/2016 00:30:00","DUNWOD",61760,21.64,1.90,0.00 21 | "02/18/2016 00:30:00","GENESE",61753,20.34,0.59,0.00 22 | "02/18/2016 00:30:00","H Q",61844,19.11,-0.63,0.00 23 | "02/18/2016 00:30:00","HUD VL",61758,21.62,1.88,0.00 24 | "02/18/2016 00:30:00","LONGIL",61762,21.90,2.15,0.00 25 | "02/18/2016 00:30:00","MHK VL",61756,20.73,0.99,0.00 26 | "02/18/2016 00:30:00","MILLWD",61759,21.66,1.91,0.00 27 | "02/18/2016 00:30:00","N.Y.C.",61761,21.72,1.97,0.00 28 | "02/18/2016 00:30:00","NORTH",61755,18.60,-1.15,0.00 29 | "02/18/2016 00:30:00","NPX",61845,21.46,1.72,0.00 30 | "02/18/2016 00:30:00","O H",61846,20.18,0.43,0.00 31 | "02/18/2016 00:30:00","PJM",61847,21.03,1.28,0.00 32 | "02/18/2016 00:30:00","WEST",61752,20.59,0.85,0.00 33 | "02/18/2016 00:45:00","CAPITL",61757,21.42,1.68,0.00 34 | "02/18/2016 00:45:00","CENTRL",61754,20.57,0.83,0.00 35 | "02/18/2016 00:45:00","DUNWOD",61760,21.62,1.88,0.00 36 | "02/18/2016 00:45:00","GENESE",61753,20.34,0.59,0.00 37 | "02/18/2016 00:45:00","H Q",61844,19.13,-0.61,0.00 38 | "02/18/2016 00:45:00","HUD VL",61758,21.62,1.88,0.00 39 | "02/18/2016 00:45:00","LONGIL",61762,21.90,2.15,0.00 40 | "02/18/2016 00:45:00","MHK VL",61756,20.73,0.99,0.00 41 | "02/18/2016 00:45:00","MILLWD",61759,21.66,1.91,0.00 42 | "02/18/2016 00:45:00","N.Y.C.",61761,21.70,1.96,0.00 43 | "02/18/2016 00:45:00","NORTH",61755,18.62,-1.12,0.00 44 | "02/18/2016 00:45:00","NPX",61845,21.46,1.72,0.00 45 | "02/18/2016 00:45:00","O H",61846,20.18,0.43,0.00 46 | "02/18/2016 00:45:00","PJM",61847,21.03,1.28,0.00 47 | "02/18/2016 00:45:00","WEST",61752,20.59,0.85,0.00 -------------------------------------------------------------------------------- /tests/fixtures/pei/chart-values.json: -------------------------------------------------------------------------------- 1 | [{"data1":150.56,"data2":4.55,"data3":0,"data4":3.09,"data5":1.46,"updateDate":1506333661,"error":0}] -------------------------------------------------------------------------------- /tests/fixtures/pjm/ForecastedLoadHistory.html: -------------------------------------------------------------------------------- 1 |

Forecasted Load History

2 |

As of 12.11.2015 17:24 EDT Refresh

3 |
4 |
Name:
5 |
PJM RTO Total
6 |
7 |
8 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Time StampMW
12.11.2015 17:00 EST92,725
12.11.2015 18:00 EST93,358
12.11.2015 19:00 EST91,496
33 |
34 |
-------------------------------------------------------------------------------- /tests/fixtures/pjm/InstantaneousLoad.html: -------------------------------------------------------------------------------- 1 |

Instantaneous Load

2 |

As of 12.11.2015 17:23 EDT Refresh

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
NameMW
PJM RTO Total91,419
Mid-Atlantic Region33,280
AE Zone1,212
-------------------------------------------------------------------------------- /tests/fixtures/sask/sysloadJSON.json: -------------------------------------------------------------------------------- 1 | { 2 | "updatedTS": "2017-07-22 8:01 pm", 3 | "currentSysLoad": "2712", 4 | "currentMonthDate": "7/10/2017", 5 | "currentMonthPeak": "3,360", 6 | "previousYearDate": "7/19/2016", 7 | "previousYearPeak": "3,217", 8 | "recordSysLoadDate": "1/12/2017", 9 | "recordSysLoad": "3,747" 10 | } 11 | -------------------------------------------------------------------------------- /tests/fixtures/sveri/api_response.csv: -------------------------------------------------------------------------------- 1 | "Time (MST)","Solar Aggregate (MW)","Wind Aggregate (MW)","Other Renewables Aggregate (MW)","Hydro Aggregate (MW)" 2 | "2015-07-18 00:00:05",0.028038,134.236,116.294,835.628 3 | "2015-07-18 00:00:15",0.079792,132.875,115.742,817.136 4 | "2015-07-18 00:00:25",0.101218,131.663,115.727,802.076 5 | "2015-07-18 00:00:35",0.119465,130.456,115.712,798.891 6 | "2015-07-18 00:00:45",0.180171,129.403,115.697,820.483 7 | "2015-07-18 00:00:55",0.268247,129.221,115.681,849.028 8 | "2015-07-18 00:01:05",0.29724,128.836,115.666,865.843 9 | "2015-07-18 00:01:15",0.281987,127.656,115.354,862.289 -------------------------------------------------------------------------------- /tests/fixtures/yukon/current_2017-10-11.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Current Energy Consumption | Yukon Energy 4 | 5 | 6 | 7 |

Current Consumption

8 |
9 |
3:40 am
10 |
Wednesday, October 11, 2017
11 |
Total Load: 38.74 MW (megawatt)
12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
    20 |
  • 0
  • 21 |
  • 10
  • 22 |
  • 20
  • 23 |
  • 30
  • 24 |
  • 40
  • 25 |
  • 50
  • 26 |
  • 60
  • 27 |
  • 70
  • 28 |
  • 80
  • 29 |
  • 90
  • 30 |
31 | 32 |
33 | 34 |
35 |
38.74 MW - hydro
36 |
71.00 MW - available hydro
37 |
38 |
39 |
40 |
41 |
    42 |
  • Available Hydro
  • 43 |
  • Hydro Usage
  • 44 |
  • Thermal Usage
  • 45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/fixtures/yukon/hourly_2017-10-11.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Consumption in the past 24 hours | Yukon Energy 4 | 5 | 6 | 78 | 79 | 80 | 81 | 82 |

Consumption in the past 24 hours

83 |
84 |
3:00 am
85 |
Wednesday, October 11, 2017
86 |
87 |
88 |

Hourly load consumption in MWh (megawatt hours)

89 | 90 |
91 | 92 | -------------------------------------------------------------------------------- /tests/fixtures/yukon/hourly_2017-11-11.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Consumption in the past 24 hours | Yukon Energy 4 | 5 | 6 | 54 | 55 | 56 | 57 | 58 |

Consumption in the past 24 hours

59 |
60 |
4:00 am
61 |
Saturday, November 11, 2017
62 |
63 |
64 |

Hourly load consumption in MWh (megawatt hours)

65 | 66 |
67 | 68 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WattTime/pyiso/c336708daf1ebbd196f5e09e706a7949f28e7fba/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/integration_test_aeso.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from unittest import TestCase 3 | 4 | from pyiso import client_factory 5 | 6 | 7 | class IntegrationTestAESO(TestCase): 8 | def setUp(self): 9 | self.aeso_client = client_factory('AESO') 10 | 11 | def test_get_generation_latest(self): 12 | generation_ts = self.aeso_client.get_generation(latest=True) 13 | 14 | self.assertEqual(len(generation_ts), 5) # Five fuels 15 | for row in generation_ts: 16 | self.assertIn(row.get('fuel_name', None), self.aeso_client.fuels) 17 | 18 | def test_get_load_yesterday(self): 19 | load_timeseries = self.aeso_client.get_load(yesterday=True) 20 | # 24 hours + 1 because BaseClient sets end_date to 00:00 of current day and 21 | # end_date must be inclusive. o_O 22 | self.assertEqual(len(load_timeseries), 25) 23 | 24 | def test_get_load_historical(self): 25 | local_now = self.aeso_client.local_now() 26 | start_at = local_now - timedelta(days=1) 27 | end_at = local_now 28 | load_timeseries = self.aeso_client.get_load(start_at=start_at, end_at=end_at) 29 | prev_entry = None 30 | for entry in load_timeseries: 31 | if prev_entry: 32 | secondsdelta = (entry['timestamp'] - prev_entry['timestamp']).total_seconds() 33 | # Each timestamp should be one hour apart. 34 | self.assertEqual(3600, secondsdelta) 35 | prev_entry = entry 36 | -------------------------------------------------------------------------------- /tests/integration/integration_test_bpa.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from pyiso import client_factory 4 | 5 | 6 | class IntegrationTestBPABase(TestCase): 7 | 8 | def test_request_latest(self): 9 | c = client_factory('BPA') 10 | response = c.request('http://transmission.bpa.gov/business/operations/wind/baltwg.txt') 11 | self.assertIn('BPA Balancing Authority Load & Total Wind Generation', response.text) 12 | -------------------------------------------------------------------------------- /tests/integration/integration_test_caiso.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | from unittest import TestCase 3 | 4 | import numpy 5 | import pytz 6 | 7 | from pyiso import client_factory 8 | 9 | 10 | class IntegrationTestCAISOClient(TestCase): 11 | def setUp(self): 12 | self.c = client_factory('CAISO') 13 | 14 | def test_request_renewable_report(self): 15 | c = client_factory('CAISO') 16 | response = self.c.request('http://content.caiso.com/green/renewrpt/20140312_DailyRenewablesWatch.txt') 17 | self.assertIn('Hourly Breakdown of Renewable Resources (MW)', response.text) 18 | 19 | def test_fetch_oasis_csv(self): 20 | ts = self.c.utcify('2014-05-08 12:00') 21 | payload = {'queryname': 'SLD_FCST', 22 | 'market_run_id': 'RTM', 23 | 'startdatetime': (ts-timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 24 | 'enddatetime': (ts+timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 25 | 'resultformat': 6, 26 | } 27 | payload.update(self.c.base_payload) 28 | data = self.c.fetch_oasis(payload=payload) 29 | self.assertEqual(len(data), 7828) 30 | self.assertIn(b'INTERVALSTARTTIME_GMT', data) 31 | 32 | def test_fetch_oasis_demand_dam(self): 33 | ts = self.c.utcify('2014-05-08 12:00') 34 | payload = {'queryname': 'SLD_FCST', 35 | 'market_run_id': 'DAM', 36 | 'startdatetime': (ts-timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 37 | 'enddatetime': (ts+timedelta(minutes=40)).strftime(self.c.oasis_request_time_format), 38 | } 39 | payload.update(self.c.base_payload) 40 | data = self.c.fetch_oasis(payload=payload) 41 | self.assertEqual(len(data), 5) 42 | self.assertEqual(str(data[0]).lower(), '\n\ 43 | SYS_FCST_DA_MW\n\ 44 | CA ISO-TAC\n\ 45 | 2014-05-08\n\ 46 | 13\n\ 47 | 2014-05-08T19:00:00-00:00\n\ 48 | 2014-05-08T20:00:00-00:00\n\ 49 | 26559.38\n\ 50 | '.lower()) 51 | 52 | def test_fetch_oasis_demand_rtm(self): 53 | ts = self.c.utcify('2014-05-08 12:00') 54 | payload = {'queryname': 'SLD_FCST', 55 | 'market_run_id': 'RTM', 56 | 'startdatetime': (ts-timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 57 | 'enddatetime': (ts+timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 58 | } 59 | payload.update(self.c.base_payload) 60 | data = self.c.fetch_oasis(payload=payload) 61 | self.assertEqual(len(data), 55) 62 | self.assertEqual(str(data[0]).lower(), '\n\ 63 | sys_fcst_15min_mw\n\ 64 | ca iso-tac\n\ 65 | 2014-05-08\n\ 66 | 49\n\ 67 | 2014-05-08t19:00:00-00:00\n\ 68 | 2014-05-08t19:15:00-00:00\n\ 69 | 26731\n\ 70 | '.lower()) 71 | 72 | def test_fetch_oasis_ren_dam(self): 73 | ts = self.c.utcify('2014-05-08 12:00') 74 | payload = {'queryname': 'SLD_REN_FCST', 75 | 'market_run_id': 'DAM', 76 | 'startdatetime': (ts-timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 77 | 'enddatetime': (ts+timedelta(minutes=40)).strftime(self.c.oasis_request_time_format), 78 | } 79 | payload.update(self.c.base_payload) 80 | data = self.c.fetch_oasis(payload=payload) 81 | self.assertEqual(len(data), 4) 82 | self.assertEqual(str(data[0]).lower(), '\n\ 83 | RENEW_FCST_DA_MW\n\ 84 | 2014-05-08\n\ 85 | 13\n\ 86 | 2014-05-08T19:00:00-00:00\n\ 87 | 2014-05-08T20:00:00-00:00\n\ 88 | 813.7\n\ 89 | NP15\n\ 90 | Solar\n\ 91 | '.lower()) 92 | 93 | def test_fetch_oasis_slrs_dam(self): 94 | ts = self.c.utcify('2014-05-08 12:00') 95 | payload = {'queryname': 'ENE_SLRS', 96 | 'market_run_id': 'DAM', 97 | 'startdatetime': (ts-timedelta(minutes=20)).strftime(self.c.oasis_request_time_format), 98 | 'enddatetime': (ts+timedelta(minutes=40)).strftime(self.c.oasis_request_time_format), 99 | } 100 | payload.update(self.c.base_payload) 101 | data = self.c.fetch_oasis(payload=payload) 102 | self.assertEqual(len(data), 17) 103 | self.assertEqual(str(data[0]).lower(), '\n\ 104 | ISO_TOT_EXP_MW\n\ 105 | Caiso_Totals\n\ 106 | 2014-05-08\n\ 107 | 13\n\ 108 | 2014-05-08T19:00:00-00:00\n\ 109 | 2014-05-08T20:00:00-00:00\n\ 110 | 1044\n\ 111 | '.lower()) 112 | -------------------------------------------------------------------------------- /tests/integration/integration_test_ercot.py: -------------------------------------------------------------------------------- 1 | from pyiso import client_factory 2 | from unittest import TestCase 3 | import pytz 4 | from datetime import datetime, timedelta 5 | import pandas as pd 6 | 7 | 8 | class IntegrationTestERCOT(TestCase): 9 | def setUp(self): 10 | self.c = client_factory('ERCOT') 11 | 12 | def test_request_report_gen_hrly(self): 13 | # get data as list of dicts 14 | df = self.c._request_report('gen_hrly') 15 | 16 | # test for expected data 17 | self.assertEqual(len(df), 1) 18 | for key in ['SE_EXE_TIME_DST', 'SE_EXE_TIME', 'SE_MW']: 19 | self.assertIn(key, df.columns) 20 | 21 | def test_request_report_wind_hrly(self): 22 | now = datetime.now(pytz.timezone(self.c.TZ_NAME)) 23 | # get data as list of dicts 24 | df = self.c._request_report('wind_hrly', now) 25 | 26 | # test for expected data 27 | self.assertLessEqual(len(df), 96) 28 | for key in ['DSTFlag', 'ACTUAL_SYSTEM_WIDE', 'HOUR_ENDING']: 29 | self.assertIn(key, df.columns) 30 | 31 | def test_request_report_load_7day(self): 32 | # get data as list of dicts 33 | df = self.c._request_report('load_7day') 34 | 35 | # test for expected data 36 | # subtract 1 hour for DST 37 | self.assertGreaterEqual(len(df), 8 * 24 - 1) 38 | for key in ['SystemTotal', 'HourEnding', 'DSTFlag', 'DeliveryDate']: 39 | self.assertIn(key, df.columns) 40 | -------------------------------------------------------------------------------- /tests/integration/integration_test_ieso.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | from unittest import TestCase 3 | from nose.plugins.skip import SkipTest 4 | 5 | import pytz 6 | 7 | from pyiso import client_factory, LOGGER 8 | 9 | 10 | class IntegrationTestIESO(TestCase): 11 | def setUp(self): 12 | self.ieso_client = client_factory('IESO') 13 | 14 | def test_get_generation_full_range(self): 15 | start_at = self.ieso_client.local_now - timedelta(days=5) 16 | end_at = self.ieso_client.local_now + timedelta(days=2) 17 | generation_ts = self.ieso_client.get_generation(start_at=start_at, end_at=end_at) 18 | self.assertFalse(len(generation_ts) == 0) 19 | 20 | def test_get_generation_yesterday(self): 21 | generation_ts = self.ieso_client.get_generation(yesterday=True) 22 | # 24 hours * 6 fuels = 144. +6 fuels because BaseClient sets end_date to 00:00 of current day and end_date must 23 | # be inclusive. 24 | self.assertEqual(len(generation_ts), 150) 25 | 26 | def test_get_generation_latest(self): 27 | generation_ts = self.ieso_client.get_generation(latest=True) 28 | self.assertEqual(len(generation_ts), 6) # 6 fuels 29 | 30 | @SkipTest 31 | def test_get_load_full_range(self): 32 | start_at = self.ieso_client.local_now - timedelta(days=3) 33 | end_at = self.ieso_client.local_now + timedelta(days=2) 34 | load_ts = self.ieso_client.get_load(start_at=start_at, end_at=end_at) 35 | self._assert_entires_5min_apart(load_ts) 36 | 37 | def test_get_load_yesterday(self): 38 | load_ts = self.ieso_client.get_load(yesterday=True) 39 | self._assert_entires_5min_apart(load_ts) 40 | # 24 hours * 12 five-minute intervals + 1 because BaseClient sets end_date to 00:00 of current day and 41 | # end_date must be inclusive. 42 | self.assertEqual(len(load_ts), 289) 43 | 44 | def test_get_load_latest(self): 45 | load_ts = self.ieso_client.get_load(latest=True) 46 | self.assertEqual(len(load_ts), 1) 47 | 48 | @SkipTest 49 | def test_get_trade_full_range(self): 50 | start_at = self.ieso_client.local_now - timedelta(days=3) 51 | end_at = self.ieso_client.local_now + timedelta(days=2) 52 | trade_ts = self.ieso_client.get_trade(start_at=start_at, end_at=end_at) 53 | self._assert_entires_5min_apart(trade_ts) 54 | 55 | def test_get_trade_yesterday(self): 56 | trade_ts = self.ieso_client.get_trade(yesterday=True) 57 | self._assert_entires_5min_apart(trade_ts) 58 | # 24 hours * 12 five-minute intervals + 1 because BaseClient sets end_date to 00:00 of current day and 59 | # end_date must be inclusive. 60 | self.assertEqual(len(trade_ts), 289) 61 | 62 | def test_get_trade_latest(self): 63 | trade_ts = self.ieso_client.get_trade(latest=True) 64 | self.assertEqual(len(trade_ts), 1) 65 | self.assertNotEqual(trade_ts[0]['net_exp_MW'], 0) 66 | 67 | def _assert_entires_5min_apart(self, result_ts): 68 | prev_entry = None 69 | for entry in result_ts: 70 | if prev_entry: 71 | seconds_delta = (entry['timestamp'] - prev_entry['timestamp']).total_seconds() 72 | if seconds_delta > 300: 73 | LOGGER.error('prev_entry timestamp: ' + str( 74 | prev_entry['timestamp'].astimezone(pytz.timezone(self.ieso_client.TZ_NAME)) 75 | )) 76 | LOGGER.error('entry timestamp: ' + str( 77 | entry['timestamp'].astimezone(pytz.timezone(self.ieso_client.TZ_NAME)) 78 | )) 79 | self.assertEqual(300, seconds_delta) 80 | prev_entry = entry 81 | -------------------------------------------------------------------------------- /tests/integration/integration_test_isone.py: -------------------------------------------------------------------------------- 1 | from pyiso import client_factory 2 | from unittest import TestCase 3 | from datetime import datetime, timedelta 4 | import pytz 5 | import dateutil.parser 6 | 7 | 8 | class IntegrationTestISONE(TestCase): 9 | def setUp(self): 10 | self.c = client_factory('ISONE') 11 | 12 | def test_genmix_json_format(self): 13 | data = self.c.fetch_data('/genfuelmix/current.json', self.c.auth) 14 | 15 | # one item, GenFuelMixes 16 | self.assertIn('GenFuelMixes', data.keys()) 17 | self.assertEqual(len(data.keys()), 1) 18 | 19 | # one item, GenFuelMix 20 | self.assertIn('GenFuelMix', data['GenFuelMixes'].keys()) 21 | self.assertEqual(len(data['GenFuelMixes'].keys()), 1) 22 | 23 | # multiple items 24 | self.assertGreater(len(data['GenFuelMixes']['GenFuelMix']), 1) 25 | self.assertEqual(sorted(data['GenFuelMixes']['GenFuelMix'][0].keys()), 26 | sorted(['FuelCategory', 'BeginDate', 'MarginalFlag', 'FuelCategoryRollup', 'GenMw'])) 27 | 28 | def test_load_realtime_json_format(self): 29 | data = self.c.fetch_data('/fiveminutesystemload/current.json', self.c.auth) 30 | 31 | # one item, FiveMinSystemLoad 32 | self.assertIn('FiveMinSystemLoad', data.keys()) 33 | self.assertEqual(len(data.keys()), 1) 34 | 35 | # several items, including date and load 36 | self.assertEqual(len(data['FiveMinSystemLoad']), 1) 37 | self.assertIn('BeginDate', data['FiveMinSystemLoad'][0].keys()) 38 | self.assertIn('LoadMw', data['FiveMinSystemLoad'][0].keys()) 39 | 40 | # values 41 | date = dateutil.parser.parse(data['FiveMinSystemLoad'][0]['BeginDate']) 42 | now = pytz.utc.localize(datetime.utcnow()) 43 | self.assertLessEqual(date, now) 44 | self.assertGreaterEqual(date, now - timedelta(minutes=6)) 45 | self.assertEqual(date.minute % 5, 0) 46 | self.assertGreater(data['FiveMinSystemLoad'][0]['LoadMw'], 0) 47 | 48 | def test_load_past_json_format(self): 49 | data = self.c.fetch_data('/fiveminutesystemload/day/20170610.json', self.c.auth) 50 | 51 | # one item, FiveMinSystemLoads 52 | self.assertIn('FiveMinSystemLoads', data.keys()) 53 | self.assertEqual(len(data.keys()), 1) 54 | self.assertIn('FiveMinSystemLoad', data['FiveMinSystemLoads'].keys()) 55 | self.assertEqual(len(data['FiveMinSystemLoads'].keys()), 1) 56 | 57 | # 288 elements: every 5 min 58 | self.assertEqual(len(data['FiveMinSystemLoads']['FiveMinSystemLoad']), 288) 59 | 60 | # several items, including date and load 61 | self.assertIn('BeginDate', data['FiveMinSystemLoads']['FiveMinSystemLoad'][0].keys()) 62 | self.assertIn('LoadMw', data['FiveMinSystemLoads']['FiveMinSystemLoad'][0].keys()) 63 | 64 | def test_load_forecast_json_format(self): 65 | # two days in future 66 | day_after_tomorrow = pytz.utc.localize(datetime.utcnow()) + timedelta(days=2) 67 | date_str = day_after_tomorrow.astimezone(pytz.timezone('US/Eastern')).strftime('%Y%m%d') 68 | data = self.c.fetch_data('/hourlyloadforecast/day/%s.json' % date_str, self.c.auth) 69 | 70 | # one item, u'HourlyLoadForecasts 71 | self.assertIn('HourlyLoadForecasts', data.keys()) 72 | self.assertEqual(len(data.keys()), 1) 73 | self.assertIn('HourlyLoadForecast', data['HourlyLoadForecasts'].keys()) 74 | self.assertEqual(len(data['HourlyLoadForecasts'].keys()), 1) 75 | 76 | # 24 elements: every 1 hr 77 | self.assertEqual(len(data['HourlyLoadForecasts']['HourlyLoadForecast']), 24) 78 | 79 | # several items, including date and load 80 | self.assertIn('BeginDate', data['HourlyLoadForecasts']['HourlyLoadForecast'][0].keys()) 81 | self.assertIn('LoadMw', data['HourlyLoadForecasts']['HourlyLoadForecast'][0].keys()) 82 | -------------------------------------------------------------------------------- /tests/integration/integration_test_nbpower.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta, datetime 2 | from unittest import TestCase 3 | 4 | import pytz 5 | 6 | from pyiso import client_factory, LOGGER 7 | 8 | 9 | class IntegrationTestNBP(TestCase): 10 | def setUp(self): 11 | self.nbpower_client = client_factory('NBP') 12 | 13 | def test_get_load_latest(self): 14 | load_ts = self.nbpower_client.get_load(latest=True) 15 | self.assertEqual(len(load_ts), 1) 16 | self.assertGreater(load_ts[0]['timestamp'], self.nbpower_client.local_now() - timedelta(hours=1)) 17 | self.assertGreater(load_ts[0]['load_MW'], 0) 18 | 19 | def test_get_load_time_range(self): 20 | start_at = pytz.utc.localize(datetime.utcnow()) - timedelta(hours=1) 21 | end_at = pytz.utc.localize(datetime.utcnow() + timedelta(hours=4)) 22 | load_ts = self.nbpower_client.get_load(start_at=start_at, end_at=end_at) 23 | self.assertGreater(len(load_ts), 0) 24 | 25 | def test_get_trade_latest(self): 26 | trade_ts = self.nbpower_client.get_trade(latest=True) 27 | self.assertEqual(len(trade_ts), 1) 28 | self.assertGreater(trade_ts[0]['timestamp'], self.nbpower_client.local_now() - timedelta(hours=1)) 29 | self.assertNotEqual(trade_ts[0]['net_exp_MW'], 0) 30 | 31 | def _assert_entries_1hr_apart(self, result_ts): 32 | prev_entry = None 33 | for entry in result_ts: 34 | if prev_entry: 35 | seconds_delta = (entry['timestamp'] - prev_entry['timestamp']).total_seconds() 36 | if seconds_delta > 3600: 37 | LOGGER.error('prev_entry timestamp: ' + str( 38 | prev_entry['timestamp'].astimezone(pytz.timezone(self.nbpower_client.TZ_NAME)) 39 | )) 40 | LOGGER.error('entry timestamp: ' + str( 41 | entry['timestamp'].astimezone(pytz.timezone(self.nbpower_client.TZ_NAME)) 42 | )) 43 | self.assertEqual(3600, seconds_delta) 44 | prev_entry = entry 45 | -------------------------------------------------------------------------------- /tests/integration/integration_test_nyiso.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import TestCase 3 | 4 | import pytz 5 | 6 | from pyiso import client_factory 7 | 8 | 9 | class IntegrationTestNYISOClient(TestCase): 10 | def setUp(self): 11 | self.c = client_factory('NYISO') 12 | 13 | def test_fetch_csv_load(self): 14 | self.c.options = {'data': 'dummy'} 15 | now = pytz.utc.localize(datetime.utcnow()) 16 | today = now.astimezone(pytz.timezone(self.c.TZ_NAME)).date() 17 | content_list = self.c.fetch_csvs(today, 'pal') 18 | self.assertEqual(len(content_list), 1) 19 | self.assertEqual(content_list[0].split('\r\n')[0], 20 | '"Time Stamp","Time Zone","Name","PTID","Load"') 21 | 22 | def test_fetch_csv_load_forecast(self): 23 | self.c.options = {'data': 'dummy'} 24 | now = pytz.utc.localize(datetime.utcnow()) 25 | today = now.astimezone(pytz.timezone(self.c.TZ_NAME)).date() 26 | content_list = self.c.fetch_csvs(today, 'isolf') 27 | self.assertEqual(len(content_list), 1) 28 | self.assertEqual(content_list[0].split('\n')[0], 29 | '"Time Stamp","Capitl","Centrl","Dunwod","Genese","Hud Vl","Longil","Mhk Vl","Millwd","N.Y.C.","North","West","NYISO"') 30 | 31 | def test_fetch_csv_trade(self): 32 | self.c.options = {'data': 'dummy'} 33 | now = pytz.utc.localize(datetime.utcnow()) 34 | today = now.astimezone(pytz.timezone(self.c.TZ_NAME)).date() 35 | content_list = self.c.fetch_csvs(today, 'ExternalLimitsFlows') 36 | self.assertEqual(len(content_list), 1) 37 | self.assertEqual(content_list[0].split('\r\n')[0], 38 | 'Timestamp,Interface Name,Point ID,Flow (MWH),Positive Limit (MWH),Negative Limit (MWH)') 39 | 40 | def test_fetch_csv_genmix(self): 41 | self.c.options = {'data': 'dummy'} 42 | now = pytz.utc.localize(datetime.utcnow()) 43 | today = now.astimezone(pytz.timezone(self.c.TZ_NAME)).date() 44 | content_list = self.c.fetch_csvs(today, 'rtfuelmix') 45 | self.assertEqual(len(content_list), 1) 46 | self.assertEqual(content_list[0].split('\r\n')[0], 47 | 'Time Stamp,Time Zone,Fuel Category,Gen MWh') 48 | -------------------------------------------------------------------------------- /tests/integration/integration_test_pjm.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from unittest import TestCase 3 | 4 | import pandas as pd 5 | import pytz 6 | 7 | from pyiso import client_factory 8 | 9 | 10 | class IntegrationTestPJMClient(TestCase): 11 | def setUp(self): 12 | self.c = client_factory('PJM') 13 | 14 | def test_fetch_edata_series_timezone(self): 15 | data = self.c.fetch_edata_series('ForecastedLoadHistory', {'name': 'PJM RTO Total'}) 16 | 17 | # check that latest forecast is within 1 hour, 1 minute of now 18 | td = data.index[0] - pytz.utc.localize(datetime.utcnow()) 19 | self.assertLessEqual(td, timedelta(hours=1, minutes=1)) 20 | 21 | def test_edata_point_rounds_to_5min(self): 22 | ts, val = self.c.fetch_edata_point('InstantaneousLoad', 'PJM RTO Total', 'MW') 23 | self.assertEqual(ts.minute % 5, 0) 24 | 25 | def test_fetch_historical_load(self): 26 | df = self.c.fetch_historical_load(2015) 27 | self.assertEqual(df['load_MW'][0], 94001.713000000003) 28 | self.assertEqual(df['load_MW'][-1], 79160.809999999998) 29 | self.assertEqual(len(df), 365*24 - 2) 30 | 31 | est = pytz.timezone(self.c.TZ_NAME) 32 | start_at = est.localize(datetime(2015, 1, 1)) 33 | end_at = est.localize(datetime(2015, 12, 31, 23, 0)) 34 | 35 | self.assertEqual(df.index[0], start_at) 36 | self.assertEqual(df.index[-1], end_at) 37 | 38 | # Manually checked form xls file 39 | # These tests should fail if timezones are improperly handled 40 | tz_func = lambda x: pd.Timestamp(pytz.utc.normalize(est.localize(x))) 41 | self.assertEqual(df.ix[tz_func(datetime(2015, 2, 2, 14))]['load_MW'], 105714.638) 42 | self.assertEqual(df.ix[tz_func(datetime(2015, 6, 4, 2))]['load_MW'], 64705.985) 43 | self.assertEqual(df.ix[tz_func(datetime(2015, 12, 15, 23))]['load_MW'], 79345.672) 44 | 45 | def test_parse_date_from_markets_operations(self): 46 | soup = self.c.fetch_markets_operations_soup() 47 | ts = self.c.parse_date_from_markets_operations(soup) 48 | td = self.c.local_now() - ts 49 | 50 | # Test that the timestamp is within the last 24 hours 51 | self.assertLess(td.total_seconds(), 60*60*24) 52 | 53 | def test_parse_realtime_genmix(self): 54 | soup = self.c.fetch_markets_operations_soup() 55 | data = self.c.parse_realtime_genmix(soup) 56 | 57 | # expect 10 fuels 58 | self.assertEqual(len(data), 10) 59 | -------------------------------------------------------------------------------- /tests/integration/integration_test_sask.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from unittest import TestCase 3 | 4 | from pyiso import client_factory 5 | 6 | 7 | class IntegrationTestSaskPowerClient(TestCase): 8 | def setUp(self): 9 | self.sask_client = client_factory('SASK') 10 | 11 | def test_get_load_latest(self): 12 | load_ts = self.sask_client.get_load(latest=True) 13 | 14 | self.assertEqual(len(load_ts), 1) 15 | self.assertLess(load_ts[0].get('timestamp', datetime.max), self.sask_client.local_now()) 16 | self.assertGreater(load_ts[0].get('load_MW', 0), 0) 17 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WattTime/pyiso/c336708daf1ebbd196f5e09e706a7949f28e7fba/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_base.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pyiso.base import BaseClient 3 | from datetime import datetime, timedelta 4 | import pytz 5 | import pandas as pd 6 | 7 | 8 | class TestBaseClient(TestCase): 9 | def test_init(self): 10 | """init creates empty options dict""" 11 | bc = BaseClient() 12 | self.assertIsNotNone(bc) 13 | self.assertEqual(len(bc.options.keys()), 0) 14 | 15 | def test_market_choices(self): 16 | """Market choices have expected values.""" 17 | bc = BaseClient() 18 | 19 | self.assertEqual('RTHR', bc.MARKET_CHOICES.hourly) 20 | self.assertEqual('RT5M', bc.MARKET_CHOICES.fivemin) 21 | self.assertEqual('DAHR', bc.MARKET_CHOICES.dam) 22 | 23 | def test_freq_choices(self): 24 | """Frequency choices have expected values.""" 25 | bc = BaseClient() 26 | 27 | self.assertEqual('1hr', bc.FREQUENCY_CHOICES.hourly) 28 | self.assertEqual('5m', bc.FREQUENCY_CHOICES.fivemin) 29 | self.assertEqual('10m', bc.FREQUENCY_CHOICES.tenmin) 30 | self.assertEqual('n/a', bc.FREQUENCY_CHOICES.na) 31 | 32 | def test_handle_options_set(self): 33 | """Can set options""" 34 | bc = BaseClient() 35 | bc.handle_options(test='a', another=20) 36 | self.assertEqual(bc.options['test'], 'a') 37 | self.assertEqual(bc.options['another'], 20) 38 | self.assertFalse(bc.options['sliceable']) 39 | 40 | def test_handle_options_latest(self): 41 | """Correct processing of time-related options for latest""" 42 | bc = BaseClient() 43 | bc.handle_options(latest=True) 44 | self.assertTrue(bc.options['latest']) 45 | self.assertFalse(bc.options['sliceable']) 46 | self.assertFalse(bc.options['forecast']) 47 | 48 | def test_handle_options_past(self): 49 | """Correct processing of time-related options for historical start and end times""" 50 | bc = BaseClient() 51 | bc.handle_options(start_at='2014-01-01', end_at='2014-02-01') 52 | self.assertTrue(bc.options['sliceable']) 53 | self.assertEqual(bc.options['start_at'], datetime(2014, 1, 1, 0, tzinfo=pytz.utc)) 54 | self.assertEqual(bc.options['end_at'], datetime(2014, 2, 1, 0, tzinfo=pytz.utc)) 55 | self.assertFalse(bc.options['forecast']) 56 | 57 | def test_handle_options_inverted_start_end(self): 58 | """Raises error if end before start""" 59 | bc = BaseClient() 60 | self.assertRaises(AssertionError, bc.handle_options, start_at='2014-02-01', end_at='2014-01-01') 61 | 62 | def test_handle_options_future(self): 63 | """Correct processing of time-related options for future start and end times""" 64 | bc = BaseClient() 65 | bc.handle_options(start_at='2100-01-01', end_at='2100-02-01') 66 | self.assertTrue(bc.options['sliceable']) 67 | self.assertEqual(bc.options['start_at'], datetime(2100, 1, 1, 0, tzinfo=pytz.utc)) 68 | self.assertEqual(bc.options['end_at'], datetime(2100, 2, 1, 0, tzinfo=pytz.utc)) 69 | self.assertTrue(bc.options['forecast']) 70 | 71 | def test_handle_options_yesterday(self): 72 | """Correct auto-setup of time-related options for yesterday""" 73 | bc = BaseClient() 74 | bc.handle_options(yesterday=True) 75 | self.assertTrue(bc.options['sliceable']) 76 | local_now = pytz.utc.localize(datetime.utcnow()).astimezone(pytz.utc) 77 | midnight_today = datetime(local_now.year, local_now.month, local_now.day, 0, tzinfo=pytz.utc) 78 | self.assertEqual(bc.options['start_at'], midnight_today - timedelta(days=1)) 79 | self.assertEqual(bc.options['end_at'], midnight_today) 80 | self.assertFalse(bc.options['forecast']) 81 | 82 | def test_handle_options_forecast(self): 83 | """Correct auto-setup of time-related options for forecast""" 84 | bc = BaseClient() 85 | bc.handle_options(forecast=True) 86 | self.assertTrue(bc.options['sliceable']) 87 | local_now = pytz.utc.localize(datetime.utcnow()).astimezone(pytz.utc).replace(microsecond=0) 88 | self.assertEqual(bc.options['start_at'], local_now) 89 | self.assertEqual(bc.options['end_at'], local_now + timedelta(days=2)) 90 | self.assertTrue(bc.options['forecast']) 91 | 92 | def test_handle_options_twice(self): 93 | """Overwrite options on second call""" 94 | bc = BaseClient() 95 | bc.handle_options(forecast=True) 96 | self.assertTrue(bc.options['forecast']) 97 | 98 | bc.handle_options(yesterday=True) 99 | self.assertFalse(bc.options['forecast']) 100 | 101 | def test_handle_options_set_forecast(self): 102 | bc = BaseClient() 103 | start = datetime(2020, 5, 26, 0, 0, tzinfo=pytz.utc) 104 | bc.handle_options(start_at=start, end_at=start+timedelta(days=2)) 105 | self.assertTrue(bc.options['forecast']) 106 | 107 | def test_bad_zipfile(self): 108 | bc = BaseClient() 109 | badzip = 'I am not a zipfile' 110 | result = bc.unzip(badzip) 111 | self.assertIsNone(result) 112 | 113 | def test_slice_empty(self): 114 | bc = BaseClient() 115 | indf = pd.DataFrame() 116 | outdf = bc.slice_times(indf, {'latest': True}) 117 | self.assertEqual(len(outdf), 0) 118 | 119 | def test_timeout(self): 120 | bc = BaseClient(timeout_seconds=30) 121 | self.assertEqual(bc.timeout_seconds, 30) 122 | -------------------------------------------------------------------------------- /tests/unit/test_bchydro.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from pandas import ExcelFile 3 | from pandas import Timestamp 4 | from unittest import TestCase 5 | from datetime import datetime, timedelta 6 | from freezegun import freeze_time 7 | from mock import MagicMock 8 | from pyiso import client_factory 9 | from pyiso.base import BaseClient 10 | from tests import fixture_path 11 | 12 | 13 | @freeze_time('2017-10-16T12:43:00Z') 14 | class TestBCHydroClient(TestCase): 15 | def setUp(self): 16 | self.tzaware_utcnow = datetime.utcnow().replace(tzinfo=pytz.utc) 17 | self.c = client_factory('BCH') 18 | 19 | def test_bchydro_from_client_factory(self): 20 | self.assertIsInstance(self.c, BaseClient) 21 | 22 | def test_get_trade_date_range_before_lower_bound_returns_empty(self): 23 | start_at = self.tzaware_utcnow - timedelta(days=11) 24 | end_at = self.tzaware_utcnow - timedelta(days=10) 25 | results = self.c.get_trade(start_at=start_at, end_at=end_at) 26 | self.assertListEqual(results, [], 'Trade results should be empty.') 27 | 28 | def test_get_trade_forecast_returns_empty(self): 29 | start_at = self.tzaware_utcnow + timedelta(seconds=1) 30 | end_at = self.tzaware_utcnow + timedelta(hours=2) 31 | results = self.c.get_trade(start_at=start_at, end_at=end_at) 32 | self.assertListEqual(results, [], 'Trade forecast results should be empty.') 33 | 34 | def test_get_trade_latest_returns_expected(self): 35 | xls_io = fixture_path(ba_name=self.c.__module__, filename='data1.xls') 36 | self.c.fetch_xls = MagicMock(return_value=ExcelFile(xls_io)) 37 | 38 | results = self.c.get_trade(latest=True) 39 | self.assertTrue(len(results), 1) 40 | self.assertEqual(results[0]['timestamp'], Timestamp('2017-10-16T12:40:00Z')) 41 | self.assertAlmostEqual(results[0]['net_exp_MW'], 964.22479248) 42 | 43 | def test_get_trade_yesterday_returns_expected(self): 44 | xls_io = fixture_path(ba_name=self.c.__module__, filename='data1.xls') 45 | self.c.fetch_xls = MagicMock(return_value=ExcelFile(xls_io)) 46 | 47 | results = self.c.get_trade(yesterday=True) 48 | self.assertTrue(len(results), 288) # 24 hours * 12 observations per hour 49 | self.assertEqual(results[0]['timestamp'], Timestamp('2017-10-15T07:00:00Z')) 50 | self.assertAlmostEqual(results[0]['net_exp_MW'], 662.693040848) 51 | self.assertEqual(results[287]['timestamp'], Timestamp('2017-10-16T06:55:00Z')) 52 | self.assertAlmostEqual(results[287]['net_exp_MW'], 700.70085144) 53 | 54 | def test_get_trade_valid_range_returns_expected(self): 55 | xls_io = fixture_path(ba_name=self.c.__module__, filename='data1.xls') 56 | self.c.fetch_xls = MagicMock(return_value=ExcelFile(xls_io)) 57 | start_at = self.tzaware_utcnow - timedelta(days=3) 58 | end_at = self.tzaware_utcnow - timedelta(days=1) 59 | 60 | results = self.c.get_trade(start_at=start_at, end_at=end_at) 61 | self.assertTrue(len(results), 576) # 48 hours * 12 observations per hour 62 | self.assertEqual(results[0]['timestamp'], Timestamp('2017-10-13T12:45:00Z')) 63 | self.assertAlmostEqual(results[0]['net_exp_MW'], 1072.920944214) 64 | self.assertEqual(results[575]['timestamp'], Timestamp('2017-10-15T12:40:00Z')) 65 | self.assertAlmostEqual(results[575]['net_exp_MW'], 506.151199341) 66 | -------------------------------------------------------------------------------- /tests/unit/test_bpa.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | from unittest import TestCase 4 | 5 | import pandas as pd 6 | import pytz 7 | 8 | from pyiso import client_factory 9 | 10 | FIXTURES_DIR = os.path.join(os.path.dirname(__file__), '../fixtures') 11 | 12 | 13 | class TestBPABase(TestCase): 14 | def setUp(self): 15 | self.wind_tsv = open(FIXTURES_DIR + '/bpa/wind_tsv.csv').read().encode('utf8') 16 | 17 | def test_parse_to_df(self): 18 | c = client_factory('BPA') 19 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 20 | index_col=0, parse_dates=True) 21 | 22 | # Date/Time set as index 23 | self.assertEqual(list(df.columns), ['Load', 'Wind', 'Hydro', 'Thermal']) 24 | 25 | # rows with NaN dropped 26 | self.assertEqual(len(df), 12) 27 | 28 | def test_utcify(self): 29 | ts_str = '04/15/2014 10:10' 30 | c = client_factory('BPA') 31 | ts = c.utcify(ts_str) 32 | self.assertEqual(ts, datetime(2014, 4, 15, 10+7, 10, tzinfo=pytz.utc)) 33 | 34 | def test_utcify_index(self): 35 | c = client_factory('BPA') 36 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 37 | index_col=0, parse_dates=True) 38 | utc_index = c.utcify_index(df.index) 39 | self.assertEqual(utc_index[0].to_pydatetime(), datetime(2014, 4, 15, 10+7, 10, tzinfo=pytz.utc)) 40 | self.assertEqual(len(df), len(utc_index)) 41 | 42 | def test_slice_latest(self): 43 | c = client_factory('BPA') 44 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 45 | index_col=0, parse_dates=True) 46 | df.index = c.utcify_index(df.index) 47 | sliced = c.slice_times(df, {'latest': True}) 48 | 49 | # values in last non-empty row 50 | self.assertEqual(list(sliced.values[0]), [6464, 3688, 10662, 1601]) 51 | 52 | def test_slice_startend(self): 53 | c = client_factory('BPA') 54 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 55 | index_col=0, parse_dates=True) 56 | df.index = c.utcify_index(df.index) 57 | sliced = c.slice_times(df, {'start_at': datetime(2014, 4, 15, 10+7, 25, tzinfo=pytz.utc), 58 | 'end_at': datetime(2014, 4, 15, 11+7, 30, tzinfo=pytz.utc)}) 59 | 60 | self.assertEqual(len(sliced), 9) 61 | self.assertEqual(list(sliced.iloc[0].values), [6537, 3684, 11281, 1601]) 62 | 63 | def test_serialize_gen(self): 64 | c = client_factory('BPA') 65 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 66 | index_col=0, parse_dates=True, usecols=[0, 2, 3, 4]) 67 | df.index = c.utcify_index(df.index) 68 | renamed = df.rename(columns=c.fuels, inplace=False) 69 | pivoted = c.unpivot(renamed) 70 | 71 | data = c.serialize(pivoted, header=['timestamp', 'fuel_name', 'gen_MW']) 72 | self.assertEqual(len(data), len(pivoted)) 73 | self.assertEqual(data[0], {'timestamp': datetime(2014, 4, 15, 17, 10, tzinfo=pytz.utc), 74 | 'gen_MW': 3732.0, 75 | 'fuel_name': 'wind'}) 76 | 77 | def test_serialize_load(self): 78 | c = client_factory('BPA') 79 | df = c.parse_to_df(self.wind_tsv, skiprows=6, header=0, delimiter='\t', 80 | index_col=0, parse_dates=True, usecols=[0, 1]) 81 | df.index = c.utcify_index(df.index) 82 | 83 | data = c.serialize(df, header=['timestamp', 'load_MW']) 84 | self.assertEqual(len(data), len(df)) 85 | self.assertEqual(data[0], {'timestamp': datetime(2014, 4, 15, 17, 10, tzinfo=pytz.utc), 'load_MW': 6553.0}) 86 | 87 | def test_parse_xls(self): 88 | c = client_factory('BPA') 89 | xd = pd.ExcelFile(FIXTURES_DIR + '/bpa/WindGenTotalLoadYTD_2014_short.xls') 90 | 91 | # parse xls 92 | df = c.parse_to_df(xd, mode='xls', sheet_names=xd.sheet_names, skiprows=18, 93 | index_col=0, parse_dates=True, 94 | parse_cols=[0, 2, 4, 5], header_names=['Wind', 'Hydro', 'Thermal'] 95 | ) 96 | self.assertEqual(list(df.columns), ['Wind', 'Hydro', 'Thermal']) 97 | self.assertGreater(len(df), 0) 98 | self.assertEqual(df.iloc[0].name, datetime(2014, 1, 1)) 99 | -------------------------------------------------------------------------------- /tests/unit/test_eia.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime, timedelta 3 | from os import environ 4 | from unittest import TestCase 5 | 6 | import mock 7 | import pytz 8 | 9 | from pyiso import client_factory 10 | from pyiso.base import BaseClient 11 | from pyiso.eia_esod import EIAClient 12 | 13 | 14 | class BALists: 15 | can_mex = ['IESO', 'BCTC', 'MHEB', 'AESO', 'HQT', 'NBSO', 'CFE', 'SPC'] 16 | no_load_bas = ['DEAA', 'EEI', 'GRIF', 'GRMA', 'GWA', 'HGMA', 'SEPA', 'WWA', 'YAD'] 17 | delay_bas = ['AEC', 'DOPD', 'GVL', 'HST', 'NSB', 'PGE', 'SCL', 'TAL', 'TIDC', 'TPWR'] 18 | seed = 28127 # Seed for random BA selection 19 | n_samples = 2 # None returns all BAs from get_BAs 20 | 21 | def __init__(self): 22 | self.us_bas = [i for i in EIAClient.EIA_BAs if i not in self.can_mex] 23 | self.load_bas = [i for i in self.us_bas if i not in self.no_load_bas] 24 | self.no_delay_bas = [i for i in self.load_bas if i not in self.delay_bas] 25 | 26 | def get_BAs(self, name, call_types=None): 27 | ''' get random sample of BA list, and exclude problem BAs ''' 28 | random.seed(self.seed) 29 | exclude_list = [] 30 | if call_types: 31 | if type(call_types) != list: 32 | call_types = [call_types] 33 | for t in call_types: 34 | exclude_list = exclude_list + getattr(self, 'problem_bas_' + t) 35 | 36 | bas = list(set(getattr(self, name)) - set(exclude_list)) 37 | if self.n_samples is None or len(bas) <= self.n_samples: 38 | return bas 39 | return random.sample(bas, self.n_samples) 40 | 41 | 42 | class TestEIA(TestCase): 43 | def setUp(self): 44 | environ['EIA_KEY'] = 'test' 45 | 46 | self.c = client_factory("EIA") 47 | self.longMessage = True 48 | self.BA_CHOICES = EIAClient.EIA_BAs 49 | self.BALists = BALists() 50 | 51 | def tearDown(self): 52 | self.c = None 53 | 54 | def _run_null_response_test(self, ba_name, data_type, **kwargs): 55 | self.c.set_ba(ba_name) # set BA name 56 | self.BALists = BALists() 57 | # mock request 58 | with mock.patch.object(self.c, 'request') as mock_request: 59 | mock_request.return_value = None 60 | # get data 61 | if data_type == "gen": 62 | data = self.c.get_generation(**kwargs) 63 | elif data_type == "trade": 64 | data = self.c.get_trade(**kwargs) 65 | elif data_type == "load": 66 | data = self.c.get_load(**kwargs) 67 | 68 | self.assertEqual(data, [], msg='BA is %s' % ba_name) 69 | 70 | def test_eiaclient_from_client_factory(self): 71 | self.assertIsInstance(self.c, BaseClient) 72 | 73 | 74 | class TestEIAGenMix(TestEIA): 75 | def test_null_response_latest(self): 76 | self._run_null_response_test(self.BALists.us_bas[0], data_type="gen", latest=True) 77 | 78 | 79 | class TestEIALoad(TestEIA): 80 | def test_null_response(self): 81 | self._run_null_response_test(self.BALists.load_bas[0], data_type="load", latest=True) 82 | 83 | def test_null_response_latest(self): 84 | self._run_null_response_test(self.BALists.load_bas[0], data_type="load", latest=True) 85 | 86 | def test_null_response_forecast(self): 87 | today = datetime.today().replace(tzinfo=pytz.utc) 88 | self._run_null_response_test(self.BALists.no_delay_bas[0], data_type="load", 89 | start_at=today + timedelta(hours=20), 90 | end_at=today+timedelta(days=2)) 91 | 92 | 93 | class TestEIATrade(TestEIA): 94 | def test_null_response(self): 95 | self._run_null_response_test(self.BALists.us_bas[0], data_type="trade", latest=True) 96 | -------------------------------------------------------------------------------- /tests/unit/test_ercot.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyiso import client_factory 4 | from unittest import TestCase 5 | import pytz 6 | from datetime import datetime 7 | 8 | 9 | FIXTURES_DIR = os.path.join(os.path.dirname(__file__), '../fixtures/ercot') 10 | 11 | 12 | class TestERCOT(TestCase): 13 | 14 | def setUp(self): 15 | self.c = client_factory('ERCOT') 16 | self.rtm_html = open(FIXTURES_DIR + '/real_time_system_conditions.html').read().encode('utf8') 17 | 18 | def test_utcify(self): 19 | ts_str = '05/03/2014 02:00' 20 | ts = self.c.utcify(ts_str) 21 | self.assertEqual(ts.year, 2014) 22 | self.assertEqual(ts.month, 5) 23 | self.assertEqual(ts.day, 3) 24 | self.assertEqual(ts.hour, 2+5) 25 | self.assertEqual(ts.minute, 0) 26 | self.assertEqual(ts.tzinfo, pytz.utc) 27 | 28 | def test_parse_load(self): 29 | self.c.handle_options(data='load', latest=True) 30 | data = self.c.parse_rtm(self.rtm_html) 31 | self.assertEqual(len(data), 1) 32 | expected_keys = ['timestamp', 'ba_name', 'load_MW', 'freq', 'market'] 33 | self.assertEqual(sorted(data[0].keys()), sorted(expected_keys)) 34 | self.assertEqual(data[0]['timestamp'], pytz.utc.localize(datetime(2016, 4, 14, 23, 38, 40))) 35 | self.assertEqual(data[0]['load_MW'], 38850.0) 36 | 37 | def test_parse_genmix(self): 38 | self.c.handle_options(data='gen', latest=True) 39 | data = self.c.parse_rtm(self.rtm_html) 40 | 41 | self.assertEqual(len(data), 2) 42 | expected_keys = ['timestamp', 'ba_name', 'gen_MW', 'fuel_name', 'freq', 'market'] 43 | self.assertEqual(sorted(data[0].keys()), sorted(expected_keys)) 44 | 45 | self.assertEqual(data[0]['timestamp'], pytz.utc.localize(datetime(2016, 4, 14, 23, 38, 40))) 46 | self.assertEqual(data[0]['gen_MW'], 5242.0) 47 | self.assertEqual(data[0]['fuel_name'], 'wind') 48 | 49 | self.assertEqual(data[1]['timestamp'], pytz.utc.localize(datetime(2016, 4, 14, 23, 38, 40))) 50 | self.assertEqual(data[1]['gen_MW'], 38850 - 5242 + 31 - 1) 51 | self.assertEqual(data[1]['fuel_name'], 'nonwind') 52 | -------------------------------------------------------------------------------- /tests/unit/test_eu.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pyiso import client_factory 3 | from unittest import TestCase 4 | import mock 5 | from mock import patch 6 | from datetime import timedelta 7 | 8 | fixtures_base_path = os.path.join(os.path.dirname(__file__), '../fixtures/eu') 9 | 10 | class TestEU(TestCase): 11 | def setUp(self): 12 | os.environ['ENTSOe_SECURITY_TOKEN'] = 'test' 13 | self.c = client_factory('EU') 14 | 15 | def test_bad_control_area(self): 16 | self.assertRaises(ValueError, self.c.get_load, 'not-a-cta', latest=True) 17 | 18 | def test_parse_resolution(self): 19 | resolution = self.c.parse_resolution('PT15M') 20 | self.assertEqual(resolution, timedelta(minutes=15)) 21 | 22 | def test_parse_load(self): 23 | self.c.handle_options(latest=True, control_area='DE(TenneT GER)', data='load') 24 | with open(os.path.join(fixtures_base_path, 'de_load.xml'), 'r') as report: 25 | parsed = self.c.parse_response(report.read().encode('ascii')) 26 | self.assertEqual(parsed[-1]['load_MW'], 13926) 27 | 28 | def test_parse_gen(self): 29 | self.c.handle_options(latest=True, control_area='DE(TenneT GER)', data='gen') 30 | with open(os.path.join(fixtures_base_path, 'de_gen.xml'), 'r') as report: 31 | parsed = self.c.parse_response(report.read().encode('ascii')) 32 | self.assertEqual(parsed[-1]['gen_MW'], 3816) 33 | self.assertEqual(parsed[-1]['fuel_name'], 'nuclear') 34 | 35 | -------------------------------------------------------------------------------- /tests/unit/test_factory.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | from pyiso import client_factory 3 | from unittest import TestCase 4 | import inspect 5 | 6 | 7 | class TestFactory(TestCase): 8 | def setUp(self): 9 | self.expected_names = [ 10 | { 11 | 'name': 'ISONE', 12 | 'env_vars': { 13 | 'ISONE_USERNAME': 'test', 14 | 'ISONE_PASSWORD': 'test', 15 | }, 16 | }, 17 | { 18 | 'name': 'MISO' 19 | }, 20 | { 21 | 'name': 'BPA', 22 | }, 23 | { 24 | 'name': 'CAISO', 25 | }, 26 | { 27 | 'name': 'ERCOT', 28 | }, 29 | { 30 | 'name': 'PJM', 31 | } 32 | ] 33 | 34 | def test_names(self): 35 | for name in self.expected_names: 36 | if 'env_vars' in name: 37 | for var in name['env_vars']: 38 | environ[var] = name['env_vars'][var]; 39 | c = client_factory(name['name']) 40 | self.assertIsNotNone(c) 41 | 42 | def test_failing(self): 43 | self.assertRaises(ValueError, client_factory, 'failing') 44 | 45 | def test_parent(self): 46 | """Test all clients are derived from BaseClient""" 47 | for name in self.expected_names: 48 | # instantiate 49 | if 'env_vars' in name: 50 | for var in name['env_vars']: 51 | environ[var] = name['env_vars'][var]; 52 | 53 | c = client_factory(name['name']) 54 | 55 | # get parent classes 56 | parent_names = [x.__name__ for x in inspect.getmro(c.__class__)] 57 | 58 | # check for BaseClient 59 | self.assertIn('BaseClient', parent_names) 60 | -------------------------------------------------------------------------------- /tests/unit/test_isone.py: -------------------------------------------------------------------------------- 1 | from os import environ, path 2 | from pyiso import client_factory 3 | from unittest import TestCase 4 | from datetime import datetime 5 | import requests_mock 6 | import pytz 7 | 8 | 9 | def read_fixture(filename): 10 | fixtures_base_path = path.join(path.dirname(__file__), '../fixtures/isone') 11 | return open(path.join(fixtures_base_path, filename), 'r').read().encode('utf-8') 12 | 13 | 14 | class TestISONE(TestCase): 15 | def setUp(self): 16 | environ['ISONE_USERNAME'] = 'test' 17 | environ['ISONE_PASSWORD'] = 'test' 18 | self.c = client_factory('ISONE') 19 | 20 | def test_auth(self): 21 | """Auth info should be set up from env during init""" 22 | self.assertEqual(len(self.c.auth), 2) 23 | 24 | def test_utcify(self): 25 | ts_str = '2014-05-03T02:32:44.000-04:00' 26 | ts = self.c.utcify(ts_str) 27 | self.assertEqual(ts.year, 2014) 28 | self.assertEqual(ts.month, 5) 29 | self.assertEqual(ts.day, 3) 30 | self.assertEqual(ts.hour, 2+4) 31 | self.assertEqual(ts.minute, 32) 32 | self.assertEqual(ts.second, 44) 33 | self.assertEqual(ts.tzinfo, pytz.utc) 34 | 35 | def test_handle_options_gen_latest(self): 36 | self.c.handle_options(data='gen', latest=True) 37 | self.assertEqual(self.c.options['market'], self.c.MARKET_CHOICES.na) 38 | self.assertEqual(self.c.options['frequency'], self.c.FREQUENCY_CHOICES.na) 39 | 40 | def test_handle_options_load_latest(self): 41 | self.c.handle_options(data='load', latest=True) 42 | self.assertEqual(self.c.options['market'], self.c.MARKET_CHOICES.fivemin) 43 | self.assertEqual(self.c.options['frequency'], self.c.FREQUENCY_CHOICES.fivemin) 44 | 45 | def test_handle_options_load_forecast(self): 46 | self.c.handle_options(data='load', forecast=True) 47 | self.assertEqual(self.c.options['market'], self.c.MARKET_CHOICES.dam) 48 | self.assertEqual(self.c.options['frequency'], self.c.FREQUENCY_CHOICES.dam) 49 | 50 | def test_endpoints_gen_latest(self): 51 | self.c.handle_options(data='gen', latest=True) 52 | endpoints = self.c.request_endpoints() 53 | self.assertEqual(len(endpoints), 1) 54 | self.assertIn('/genfuelmix/current.json', endpoints) 55 | 56 | def test_endpoints_load_latest(self): 57 | self.c.handle_options(data='load', latest=True) 58 | endpoints = self.c.request_endpoints() 59 | self.assertEqual(len(endpoints), 1) 60 | self.assertIn('/fiveminutesystemload/current.json', endpoints) 61 | 62 | def test_endpoints_gen_range(self): 63 | self.c.handle_options(data='gen', 64 | start_at=pytz.utc.localize(datetime(2016, 5, 2, 12)), 65 | end_at=pytz.utc.localize(datetime(2016, 5, 2, 14))) 66 | endpoints = self.c.request_endpoints() 67 | self.assertEqual(len(endpoints), 1) 68 | self.assertIn('/genfuelmix/day/20160502.json', endpoints) 69 | 70 | def test_endpoints_load_range(self): 71 | self.c.handle_options(data='load', 72 | start_at=pytz.utc.localize(datetime(2016, 5, 2, 12)), 73 | end_at=pytz.utc.localize(datetime(2016, 5, 2, 14))) 74 | endpoints = self.c.request_endpoints() 75 | self.assertEqual(len(endpoints), 1) 76 | self.assertIn('/fiveminutesystemload/day/20160502.json', endpoints) 77 | 78 | def test_endpoints_load_forecast(self): 79 | self.c.handle_options(data='load', forecast=True) 80 | endpoints = self.c.request_endpoints() 81 | self.assertGreaterEqual(len(endpoints), 3) 82 | self.assertIn('/hourlyloadforecast/day/', endpoints[0]) 83 | 84 | now = pytz.utc.localize(datetime.utcnow()).astimezone(pytz.timezone('US/Eastern')) 85 | date_str = now.strftime('%Y%m%d') 86 | self.assertIn(date_str, endpoints[0]) 87 | 88 | @requests_mock.Mocker() 89 | def test_get_morningreport(self, mock_request): 90 | expected_url = 'https://webservices.iso-ne.com/api/v1.1/morningreport/current.json' 91 | expected_response = read_fixture('morningreport_current.json') 92 | mock_request.get(expected_url, content=expected_response) 93 | 94 | resp = self.c.get_morningreport() 95 | assert "MorningReports" in resp 96 | 97 | @requests_mock.Mocker() 98 | def test_get_morningreport_for_day(self, mock_request): 99 | expected_url = 'https://webservices.iso-ne.com/api/v1.1/morningreport/day/20160101.json' 100 | expected_response = read_fixture('morningreport_20160101.json') 101 | mock_request.get(expected_url, content=expected_response) 102 | 103 | resp = self.c.get_morningreport(day="20160101") 104 | assert resp["MorningReports"]["MorningReport"][0]["BeginDate"] == "2016-01-01T00:00:00.000-05:00" 105 | 106 | def test_get_morningreport_bad_date(self): 107 | self.assertRaises(ValueError, self.c.get_morningreport, day="foo") 108 | 109 | @requests_mock.Mocker() 110 | def test_get_sevendayforecast(self, mock_request): 111 | expected_url = 'https://webservices.iso-ne.com/api/v1.1/sevendayforecast/current.json' 112 | expected_response = read_fixture('sevendayforecast_current.json') 113 | mock_request.get(expected_url, content=expected_response) 114 | 115 | resp = self.c.get_sevendayforecast() 116 | assert "SevenDayForecasts" in resp 117 | 118 | @requests_mock.Mocker() 119 | def test_get_sevendayforecast_for_day(self, mock_request): 120 | expected_url = 'https://webservices.iso-ne.com/api/v1.1/sevendayforecast/day/20160101.json' 121 | expected_response = read_fixture('sevendayforecast_20160101.json') 122 | mock_request.get(expected_url, content=expected_response) 123 | 124 | resp = self.c.get_sevendayforecast(day="20160101") 125 | assert resp["SevenDayForecasts"]["SevenDayForecast"][0]["BeginDate"] == "2016-01-01T00:00:00.000-05:00" 126 | 127 | def test_get_sevendayforecast_bad_date(self): 128 | self.assertRaises(ValueError, self.c.get_sevendayforecast, day="foo") 129 | -------------------------------------------------------------------------------- /tests/unit/test_miso.py: -------------------------------------------------------------------------------- 1 | from pyiso import client_factory 2 | from unittest import TestCase 3 | import pytz 4 | 5 | 6 | class TestMISO(TestCase): 7 | def setUp(self): 8 | self.c = client_factory('MISO') 9 | 10 | def test_utcify(self): 11 | ts_str = '2014-05-03T01:45:00' 12 | ts = self.c.utcify(ts_str) 13 | self.assertEqual(ts.year, 2014) 14 | self.assertEqual(ts.month, 5) 15 | self.assertEqual(ts.day, 3) 16 | self.assertEqual(ts.hour, 1+5) 17 | self.assertEqual(ts.minute, 45) 18 | self.assertEqual(ts.tzinfo, pytz.utc) 19 | 20 | def test_parse_latest_fuel_mix_bad(self): 21 | bad_content = b'header1,header2\n\nnotadate,2016-01-01' 22 | data = self.c.parse_latest_fuel_mix(bad_content) 23 | self.assertEqual(len(data), 0) 24 | -------------------------------------------------------------------------------- /tests/unit/test_nlhydro.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | from unittest import TestCase 3 | from pandas import Timestamp 4 | from pyiso import client_factory 5 | from pyiso.base import BaseClient 6 | from tests import read_fixture 7 | 8 | 9 | class TestNLHydroClient(TestCase): 10 | def setUp(self): 11 | self.c = client_factory('NLH') 12 | 13 | def test_nlhydro_from_client_factory(self): 14 | self.assertIsInstance(self.c, BaseClient) 15 | 16 | @requests_mock.Mocker() 17 | def test_get_load_latest(self, mocked_request): 18 | response_str = read_fixture(self.c.__module__, 'system-information-center.html') 19 | expected_response = response_str.encode('utf-8') 20 | mocked_request.get(self.c.SYSTEM_INFO_URL, content=expected_response) 21 | 22 | load_ts = self.c.get_load(latest=True) 23 | self.assertEqual(len(load_ts), 1) 24 | self.assertEqual(load_ts[0].get('timestamp', None), Timestamp('2017-10-20T01:45:00Z')) 25 | self.assertEqual(load_ts[0].get('load_MW', None), 773) 26 | -------------------------------------------------------------------------------- /tests/unit/test_nyiso.py: -------------------------------------------------------------------------------- 1 | from pyiso import client_factory 2 | from unittest import TestCase 3 | from datetime import date 4 | 5 | from tests import read_fixture 6 | 7 | 8 | class TestNYISOBase(TestCase): 9 | def setUp(self): 10 | self.c = client_factory('NYISO') 11 | 12 | def test_parse_load_rtm(self): 13 | self.c.options = {'data': 'dummy'} 14 | actual_load_csv = read_fixture(ba_name='nyiso', filename='20171122pal.csv') 15 | df = self.c.parse_load_rtm(actual_load_csv.encode('utf-8')) 16 | for idx, row in df.iterrows(): 17 | self.assertIn(idx.date(), [date(2017, 11, 22), date(2017, 11, 23)]) 18 | self.assertGreater(row['load_MW'], 13000) 19 | self.assertLess(row['load_MW'], 22000) 20 | self.assertEqual(df.index.name, 'timestamp') 21 | 22 | def test_parse_load_forecast(self): 23 | self.c.options = {'data': 'dummy'} 24 | load_forecast_csv = read_fixture(ba_name='nyiso', filename='20171122isolf.csv') 25 | data = self.c.parse_load_forecast(load_forecast_csv.encode('utf-8')) 26 | for idx, row in data.iterrows(): 27 | self.assertGreaterEqual(idx.date(), date(2017, 11, 22)) 28 | self.assertLessEqual(idx.date(), date(2017, 11, 28)) 29 | self.assertGreater(row['load_MW'], 12000) 30 | self.assertLess(row['load_MW'], 22000) 31 | # should have 6 days of hourly data 32 | self.assertEqual(len(data), 24*6) 33 | 34 | def test_parse_trade(self): 35 | self.c.options = {'data': 'dummy'} 36 | external_limits_flows_csv = read_fixture(ba_name='nyiso', filename='20171122ExternalLimitsFlows.csv') 37 | df = self.c.parse_trade(external_limits_flows_csv.encode('utf-8')) 38 | 39 | for idx, row in df.iterrows(): 40 | self.assertIn(idx.date(), [date(2017, 11, 22), date(2017, 11, 23)]) 41 | self.assertLess(row['net_exp_MW'], -1400) 42 | self.assertGreater(row['net_exp_MW'], -6300) 43 | self.assertEqual(df.index.name, 'timestamp') 44 | 45 | def test_parse_genmix(self): 46 | self.c.options = {'data': 'dummy'} 47 | rtfuelmix_csv = read_fixture(ba_name='nyiso', filename='20171122rtfuelmix.csv') 48 | df = self.c.parse_genmix(rtfuelmix_csv.encode('utf-8')) 49 | 50 | for idx, row in df.iterrows(): 51 | self.assertIn(idx.date(), [date(2017, 11, 22), date(2017, 11, 23)]) 52 | self.assertLess(row['gen_MW'], 5500) 53 | self.assertIn(row['fuel_name'], self.c.fuel_names.values()) 54 | self.assertEqual(df.index.name, 'timestamp') 55 | 56 | def test_parse_legacy_genmix(self): 57 | """ 58 | Tests that legacy generation mix data format can still be parsed if someone requests a historical time range. 59 | """ 60 | self.c.options = {'data': 'dummy'} 61 | legacy_ftfuelmix_csv = read_fixture(ba_name='nyiso', filename='20160119rtfuelmix.csv') 62 | df = self.c.parse_genmix(legacy_ftfuelmix_csv.encode('utf-8')) 63 | 64 | for idx, row in df.iterrows(): 65 | self.assertIn(idx.date(), [date(2016, 1, 19), date(2016, 1, 20)]) 66 | self.assertLess(row['gen_MW'], 5500) 67 | self.assertIn(row['fuel_name'], self.c.fuel_names.values()) 68 | self.assertEqual(df.index.name, 'timestamp') 69 | -------------------------------------------------------------------------------- /tests/unit/test_pei.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | from unittest import TestCase 3 | from pandas import Timestamp 4 | from pyiso import client_factory 5 | from pyiso.base import BaseClient 6 | from tests import read_fixture 7 | 8 | 9 | class TestPEIClient(TestCase): 10 | def setUp(self): 11 | self.c = client_factory('PEI') 12 | 13 | def test_pei_retrievable_from_client_factory(self): 14 | self.assertIsInstance(self.c, BaseClient) 15 | 16 | @requests_mock.Mocker() 17 | def test_get_load_success(self, mock_request): 18 | expected_response = read_fixture(self.c.NAME, 'chart-values.json').encode('utf8') 19 | mock_request.get('http://www.gov.pe.ca/windenergy/chart-values.php', content=expected_response) 20 | 21 | load_ts = self.c.get_load(latest=True) 22 | 23 | self.assertEqual(len(load_ts), 1) 24 | self.assertEqual(load_ts[0].get('timestamp', None), Timestamp('2017-09-25T10:01:01.000Z')) 25 | self.assertEqual(load_ts[0].get('load_MW', None), 150.56) 26 | 27 | @requests_mock.Mocker() 28 | def test_get_generation_success(self, mock_request): 29 | expected_response = read_fixture(self.c.NAME, 'chart-values.json').encode('utf8') 30 | mock_request.get('http://www.gov.pe.ca/windenergy/chart-values.php', content=expected_response) 31 | expected_timestamp = Timestamp('2017-09-25T10:01:01.000Z') 32 | 33 | load_ts = self.c.get_generation(latest=True) 34 | 35 | self.assertEqual(len(load_ts), 3) 36 | self.assertEqual(load_ts[0].get('timestamp', None), expected_timestamp) 37 | self.assertEqual(load_ts[0].get('fuel_name', None), 'oil') 38 | self.assertEqual(load_ts[0].get('gen_MW', None), 0) 39 | self.assertEqual(load_ts[1].get('timestamp', None), expected_timestamp) 40 | self.assertEqual(load_ts[1].get('fuel_name', None), 'other') 41 | self.assertEqual(load_ts[1].get('gen_MW', None), 146.01) 42 | self.assertEqual(load_ts[2].get('timestamp', None), expected_timestamp) 43 | self.assertEqual(load_ts[2].get('fuel_name', None), 'wind') 44 | self.assertEqual(load_ts[2].get('gen_MW', None), 4.55) 45 | -------------------------------------------------------------------------------- /tests/unit/test_pjm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyiso import client_factory 4 | from unittest import TestCase 5 | import pandas as pd 6 | from datetime import datetime, timedelta 7 | import pytz 8 | 9 | FIXTURES_DIR = os.path.join(os.path.dirname(__file__), '../fixtures') 10 | 11 | 12 | class TestPJM(TestCase): 13 | def setUp(self): 14 | self.edata_inst_load = open(FIXTURES_DIR + '/pjm/InstantaneousLoad.html').read().encode('utf8') 15 | self.edata_forecast_load = open(FIXTURES_DIR + '/pjm/ForecastedLoadHistory.html').read().encode('utf8') 16 | self.c = client_factory('PJM') 17 | 18 | def test_utcify_pjmlike_edt(self): 19 | ts_str = '04/13/14 21:45 EDT' 20 | ts = self.c.utcify(ts_str) 21 | self.assertEqual(ts.year, 2014) 22 | self.assertEqual(ts.month, 4) 23 | self.assertEqual(ts.day, 13+1) 24 | self.assertEqual(ts.hour, 21-20) 25 | self.assertEqual(ts.minute, 45) 26 | self.assertEqual(ts.tzinfo, pytz.utc) 27 | 28 | def test_utcify_pjmlike_est(self): 29 | ts_str = '11/13/14 21:45 EST' 30 | ts = self.c.utcify(ts_str) 31 | self.assertEqual(ts.year, 2014) 32 | self.assertEqual(ts.month, 11) 33 | self.assertEqual(ts.day, 13+1) 34 | self.assertEqual(ts.hour, 21-19) 35 | self.assertEqual(ts.minute, 45) 36 | self.assertEqual(ts.tzinfo, pytz.utc) 37 | 38 | def test_time_as_of(self): 39 | ts = self.c.time_as_of(self.edata_inst_load) 40 | self.assertEqual(ts, datetime(2015, 12, 11, 17, 23, tzinfo=pytz.utc) + timedelta(hours=5)) 41 | 42 | def test_parse_inst_load(self): 43 | dfs = pd.read_html(self.edata_inst_load, header=0, index_col=0) 44 | df = dfs[0] 45 | self.assertEqual(df.columns, 'MW') 46 | self.assertEqual(df.loc['PJM RTO Total']['MW'], 91419) 47 | 48 | def test_missing_time_is_none(self): 49 | ts = self.c.time_as_of('') 50 | self.assertIsNone(ts) 51 | 52 | def test_bad_url(self): 53 | ts, val = self.c.fetch_edata_point('badtype', 'badkey', 'badheader') 54 | self.assertIsNone(ts) 55 | self.assertIsNone(val) 56 | -------------------------------------------------------------------------------- /tests/unit/test_sask.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests_mock 3 | from pandas import Timestamp 4 | from pyiso import client_factory 5 | from pyiso.base import BaseClient 6 | from unittest import TestCase 7 | 8 | FIXTURES_DIR = os.path.join(os.path.dirname(__file__), '../fixtures') 9 | 10 | 11 | class TestSASK(TestCase): 12 | def setUp(self): 13 | self.c = client_factory('SASK') 14 | 15 | def test_sask_from_client_factory(self): 16 | self.assertIsInstance(self.c, BaseClient) 17 | 18 | @requests_mock.Mocker() 19 | def test_get_load_latest(self, mocked_request): 20 | expected_response = open(FIXTURES_DIR + '/sask/sysloadJSON.json').read().encode('utf8') 21 | mocked_request.get(self.c.SYSLOAD_URL, content=expected_response) 22 | 23 | load_ts = self.c.get_load(latest=True) 24 | 25 | self.assertEqual(len(load_ts), 1) 26 | self.assertEqual(load_ts[0].get('timestamp', None), Timestamp('2017-07-23T02:01:00.000Z')) 27 | self.assertEqual(load_ts[0].get('load_MW', None), 2712) 28 | -------------------------------------------------------------------------------- /tests/unit/test_sveri.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyiso import client_factory 4 | from unittest import TestCase 5 | from datetime import time, datetime, timedelta 6 | import pytz 7 | 8 | FIXTURES_DIR = os.path.join(os.path.dirname(__file__), '../fixtures/sveri') 9 | 10 | 11 | class TestSVERI(TestCase): 12 | def setUp(self): 13 | self.c = client_factory('AZPS') 14 | self.TZ_NAME = 'America/Phoenix' 15 | 16 | self.now = datetime.now(pytz.timezone(self.TZ_NAME)) 17 | self.today = datetime.combine(self.now.date(), time()) 18 | self.yesterday = self.today - timedelta(days=1) 19 | self.tomorrow = self.today + timedelta(days=1) 20 | self.last_month = self.today - timedelta(days=32) 21 | self.next_month = self.today + timedelta(days=32) 22 | 23 | self.sample = open(FIXTURES_DIR + '/api_response.csv').read().encode('utf8') 24 | self.sample_start = pytz.timezone(self.TZ_NAME).localize(datetime(2015, 7, 18, 0, 0)) 25 | self.sample_end = pytz.timezone(self.TZ_NAME).localize(datetime(2015, 7, 19, 0, 0)) 26 | 27 | def test_parse_to_df(self): 28 | df = self.c.parse_to_df(self.sample, header=0, parse_dates=True, date_parser=self.c.date_parser, index_col=0) 29 | self.assertEquals(df.index.name, "Time (MST)") 30 | self.assertEquals(df.index.dtype, 'datetime64[ns]') 31 | headers = ["Solar Aggregate (MW)", "Wind Aggregate (MW)", "Other Renewables Aggregate (MW)", "Hydro Aggregate (MW)"] 32 | self.assertEquals(list(df.columns.values), headers) 33 | self.assertEquals(len(df.index), len(self.sample.splitlines()) - 1) 34 | 35 | def test_clean_df_gen(self): 36 | self.c.handle_options(data='gen', latest=False, yesterday=False, 37 | start_at=self.sample_start, end_at=self.sample_end) 38 | parsed_df = self.c.parse_to_df(self.sample, header=0, parse_dates=True, date_parser=self.c.date_parser, index_col=0) 39 | cleaned_df = self.c.clean_df(parsed_df) 40 | headers = ["fuel_name", "gen_MW"] 41 | self.assertEquals(list(cleaned_df.columns.values), headers) 42 | self.assertTrue((cleaned_df.index == self.c.utcify(parsed_df.index[0])).all()) 43 | self.assertEquals(cleaned_df['fuel_name'].tolist(), ['solar', 'wind', 'renewable', 'hydro']) 44 | self.assertEquals(cleaned_df['gen_MW'].tolist(), parsed_df.ix[0].tolist()) 45 | 46 | def test_serialize(self): 47 | self.c.handle_options(data='gen', latest=False, yesterday=False, 48 | start_at=self.sample_start, end_at=self.sample_end) 49 | parsed_df = self.c.parse_to_df(self.sample, header=0, parse_dates=True, date_parser=self.c.date_parser, index_col=0) 50 | cleaned_df = self.c.clean_df(parsed_df) 51 | extras = { 52 | 'ba_name': self.c.NAME, 53 | 'market': self.c.MARKET_CHOICES.fivemin, 54 | 'freq': self.c.FREQUENCY_CHOICES.fivemin 55 | } 56 | result = self.c.serialize_faster(cleaned_df, extras) 57 | for index, dp in enumerate(result): 58 | self.assertEquals(dp['fuel_name'], cleaned_df['fuel_name'].tolist()[index]) 59 | self.assertEquals(dp['gen_MW'], cleaned_df['gen_MW'].tolist()[index]) 60 | self.assertEquals(dp['timestamp'], cleaned_df.index.tolist()[index]) 61 | self.assertEquals(dp['ba_name'], extras['ba_name']) 62 | self.assertEquals(dp['market'], extras['market']) 63 | self.assertEquals(dp['freq'], extras['freq']) 64 | 65 | def test_get_payloads_latest(self): 66 | # latest 67 | self.c.handle_options(data='gen', latest=True, yesterday=False, 68 | start_at=False, end_at=False) 69 | start = self.today.strftime('%Y-%m-%d') 70 | end = self.tomorrow.strftime('%Y-%m-%d') 71 | result = { # gen 72 | 'ids': '1,2,3,4', 73 | 'startDate': start, 74 | 'endDate': end, 75 | 'saveData': 'true' 76 | } 77 | result2 = result.copy() # gen 78 | result2['ids'] = '5,6,7,8' 79 | self.assertEquals(self.c.get_gen_payloads(), (result, result2)) 80 | 81 | result['ids'] = '0' # load 82 | self.assertEquals(self.c.get_load_payload(), result) 83 | 84 | def test_get_payloads(self): 85 | # variable start/end 86 | self.c.handle_options(data='gen', latest=True, yesterday=False, 87 | start_at=self.yesterday, end_at=self.today) 88 | start = self.yesterday.strftime('%Y-%m-%d') 89 | end = self.today.strftime('%Y-%m-%d') 90 | result = { # gen 91 | 'ids': '1,2,3,4', 92 | 'startDate': start, 93 | 'endDate': end, 94 | 'saveData': 'true' 95 | } 96 | result2 = result.copy() # gen 97 | result2['ids'] = '5,6,7,8' 98 | self.assertEquals(self.c.get_gen_payloads(), (result, result2)) 99 | 100 | result['ids'] = '0' # load 101 | self.assertEquals(self.c.get_load_payload(), result) 102 | --------------------------------------------------------------------------------