diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml deleted file mode 100644 index ccc46f0..0000000 --- a/.github/workflows/flake8.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Flake8 - -on: [push] - -jobs: - flake8: - runs-on: ubuntu-20.04 - - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v2 - - - name: Set up Python 3.8 - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 - with: - python-version: 3.8 - - - name: Install Pip Dependencies - run: pip install flake8 - - - name: Run Flake8 - run: flake8 energyplus_api_helpers diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..530ccca --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +name: Lint + +on: [push] + +jobs: + lint: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Check precommit hooks + uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed4e75d..868fbad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,7 @@ name: PyPIRelease on: push: + branches: [ main ] tags: - '*' @@ -10,23 +11,24 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 + uses: actions/setup-python@v4 with: python-version: 3.8 - name: Install Pip Dependencies shell: bash - run: pip install -r requirements.txt + run: pip install --upgrade build - name: Build the Wheel shell: bash - run: rm -rf dist/ build/ && python3 setup.py bdist_wheel sdist + run: rm -rf dist/ build/ && python3 -m build - name: Deploy on Test PyPi - uses: pypa/gh-action-pypi-publish@37f50c210e3d2f9450da2cd423303d6a14a6e29f # v1.5.1 + if: contains(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPIPW }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e078db3..f39ff10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,24 +32,34 @@ jobs: wget: 'C:\msys64\usr\bin\wget.exe' # -q GitHub actions can't find wget at this location runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v2 + + - uses: actions/checkout@v4 + - name: Set up Python 3.8 - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 + uses: actions/setup-python@v4 with: python-version: 3.8 + - name: Install Python Dependencies - run: pip install pysparklines + run: pip install -e . + - name: Download EnergyPlus run: ${{ matrix.wget }} "https://github.com/NREL/EnergyPlus/releases/download/v22.2.0/${{ matrix.file_base_name }}${{ matrix.file_extension }}" -O "energyplus${{ matrix.file_extension }}" + - name: Extract EnergyPlus run: ${{ matrix.extract_command }} "energyplus${{ matrix.file_extension }}" + - name: Run Example Script 01 run: python ./energyplus_api_helpers/demos/01_simple_library_call.py "./${{ matrix.file_base_name }}" + # - name: Run Example Script 02 # run: python ./energyplus_api_helpers/demos/02_threaded.py "./${{ matrix.file_base_name }}" +# - name: Run Example Script 03 run: python ./energyplus_api_helpers/demos/03_multiprocessed.py "./${{ matrix.file_base_name }}" + - name: Run Example Script 04 run: python ./energyplus_api_helpers/demos/04_dynamic_terminal_output_progress.py "./${{ matrix.file_base_name }}" + - name: Run Example Script 05 run: python ./energyplus_api_helpers/demos/05_dynamic_terminal_output.py "./${{ matrix.file_base_name }}" diff --git a/.gitignore b/.gitignore index c56d284..3da4486 100644 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,3 @@ cython_debug/ .vscode *.code-workspace - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..daeded1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.5.1 + hooks: + - id: mypy +- repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-pyproject] diff --git a/README.md b/README.md index a81d492..80d5338 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # EnergyPlus API Helper Scripts +[![pypi](https://img.shields.io/pypi/v/energyplus-api-helpers.svg)](https://pypi.org/project/energyplus-api-helpers/) +[![python](https://img.shields.io/pypi/pyversions/energyplus-api-helpers.svg)](https://pypi.org/project/energyplus-api-helpers/) +[![Build Status](https://github.com/Myoldmopar/EnergyPlusAPIHelper/actions/workflows/test.yml/badge.svg)](https://github.com/Myoldmopar/EnergyPlusAPIHelper/actions/workflows/test.yml) + This project is a small library of helper functionality and, more importantly, demo scripts, for interacting with the EnergyPlus API. The EnergyPlus Python API is not on PyPi (as of now), it simply comes with the EnergyPlus installation. This library makes that process a bit easier, and also offers a set of demos in the `energyplus_api_helpers/demos` folder. +## Usage + A super minimal example using the helper class here: ```python @@ -20,16 +26,40 @@ In this example, the helper class is constructed simply by pointing it to a vali The helper class is then used to get an EnergyPlus API instance, which is in turn used to create a new EnergyPlus "state". Finally, EnergyPlus is executed with some basic command line arguments passed into the `run_energyplus` function of the main EnergyPlus API. +### Inferring EnergyPlus path + +It is possible to call `helper = EnergyPlusHelper()` (without a path argument), in which case the helper tries to locate your E+ installation directory. + +It does so checking, in order of preference: + +* Any `energyplus` executable in your `PATH` +* By trying to locate the most recent EnergyPlus version installed in a default location + ## Code Quality -[![Flake8](https://github.com/Myoldmopar/EnergyPlusAPIDemos/actions/workflows/flake8.yml/badge.svg)](https://github.com/Myoldmopar/EnergyPlusAPIDemos/actions/workflows/flake8.yml) +[![Lint](https://github.com/Myoldmopar/EnergyPlusAPIHelper/actions/workflows/lint.yml/badge.svg)](https://github.com/Myoldmopar/EnergyPlusAPIHelper/actions/workflows/lint.yml) + +Code is checked for style using GitHub Actions (flake8, black, isort, mypy). + +## Contributing + +You should install this project along with development dependencies via: + +``` +pip install -e .[dev] +``` + +There is a pre-commit configuration you should install if you plan on contributing: + +``` +pre-commit install +``` -Code is checked for style using GitHub Actions. ## Releases -[![PyPIRelease](https://github.com/Myoldmopar/EnergyPlusAPIDemos/actions/workflows/release.yml/badge.svg)](https://github.com/Myoldmopar/EnergyPlusAPIDemos/actions/workflows/release.yml) +[![PyPIRelease](https://github.com/Myoldmopar/EnergyPlusAPIHelper/actions/workflows/release.yml/badge.svg)](https://github.com/Myoldmopar/EnergyPlusAPIDemos/actions/workflows/release.yml) When a release is tagged, a GitHub Action workflow will create a Python wheel and upload it to the PyPi server. -To install into an existing Python environment, execute `pip install energyplus_api_helpers` +To install into an existing Python environment, execute `pip install energyplus-api-helpers` diff --git a/energyplus_api_helpers/demos/01_simple_library_call.py b/energyplus_api_helpers/demos/01_simple_library_call.py index 5c44c5f..e253c29 100644 --- a/energyplus_api_helpers/demos/01_simple_library_call.py +++ b/energyplus_api_helpers/demos/01_simple_library_call.py @@ -1,22 +1,9 @@ -from pathlib import Path -from sys import argv +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper - -eplus_path = '/eplus/installs/EnergyPlus-22-2-0' -if len(argv) > 1: - eplus_path = argv[1] - -e = EPlusAPIHelper(Path(eplus_path)) +e = EPlusAPIHelper(get_eplus_path_from_argv1()) api = e.get_api_instance() state = api.state_manager.new_state() return_value = api.runtime.run_energyplus( - state, [ - '-d', - e.get_temp_run_dir(), - '-a', - '-w', - e.weather_file_path(), - e.path_to_test_file('5ZoneAirCooled.idf') - ] + state, ["-d", e.get_temp_run_dir(), "-a", "-w", e.weather_file_path(), e.path_to_test_file("5ZoneAirCooled.idf")] ) diff --git a/energyplus_api_helpers/demos/02_threaded.py b/energyplus_api_helpers/demos/02_threaded.py index 5ebfa61..40ea54c 100644 --- a/energyplus_api_helpers/demos/02_threaded.py +++ b/energyplus_api_helpers/demos/02_threaded.py @@ -1,30 +1,18 @@ -from pathlib import Path -from sys import argv from threading import Thread -from energyplus_api_helpers.import_helper import EPlusAPIHelper - -eplus_path = '/eplus/installs/EnergyPlus-22-2-0' -if len(argv) > 1: - eplus_path = argv[1] +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 +from energyplus_api_helpers.import_helper import EPlusAPIHelper def thread_function(_working_dir: str): print(f"Thread: Running at working dir: {_working_dir}") state = api.state_manager.new_state() api.runtime.run_energyplus( - state, [ - '-d', - _working_dir, - '-a', - '-w', - e.weather_file_path(), - e.path_to_test_file('5ZoneAirCooled.idf') - ] + state, ["-d", _working_dir, "-a", "-w", e.weather_file_path(), e.path_to_test_file("5ZoneAirCooled.idf")] ) -e = EPlusAPIHelper(Path(eplus_path)) +e = EPlusAPIHelper(get_eplus_path_from_argv1()) api = e.get_api_instance() for index in range(3): working_dir = e.get_temp_run_dir() diff --git a/energyplus_api_helpers/demos/03_multiprocessed.py b/energyplus_api_helpers/demos/03_multiprocessed.py index 96bfea1..b1311d2 100644 --- a/energyplus_api_helpers/demos/03_multiprocessed.py +++ b/energyplus_api_helpers/demos/03_multiprocessed.py @@ -1,28 +1,17 @@ -from pathlib import Path import multiprocessing as mp -from sys import argv -from energyplus_api_helpers.import_helper import EPlusAPIHelper -eplus_path = '/eplus/installs/EnergyPlus-22-2-0' -if len(argv) > 1: - eplus_path = argv[1] +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 +from energyplus_api_helpers.import_helper import EPlusAPIHelper def subprocess_function(): - e = EPlusAPIHelper(Path(eplus_path)) + e = EPlusAPIHelper(get_eplus_path_from_argv1()) api = e.get_api_instance() working_dir = e.get_temp_run_dir() print(f"Thread: Running at working dir: {working_dir}") state = api.state_manager.new_state() api.runtime.run_energyplus( - state, [ - '-d', - working_dir, - '-a', - '-w', - e.weather_file_path(), - e.path_to_test_file('1ZoneUncontrolled.idf') - ] + state, ["-d", working_dir, "-a", "-w", e.weather_file_path(), e.path_to_test_file("1ZoneUncontrolled.idf")] ) diff --git a/energyplus_api_helpers/demos/04_dynamic_terminal_output_progress.py b/energyplus_api_helpers/demos/04_dynamic_terminal_output_progress.py index 82c969a..6ddd711 100644 --- a/energyplus_api_helpers/demos/04_dynamic_terminal_output_progress.py +++ b/energyplus_api_helpers/demos/04_dynamic_terminal_output_progress.py @@ -1,33 +1,20 @@ -from pathlib import Path -from sys import argv +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper -eplus_path = '/eplus/installs/EnergyPlus-22-2-0' -if len(argv) > 1: - eplus_path = argv[1] - def progress_update(percent): filled_length = int(80 * (percent / 100.0)) - bar = "*" * filled_length + '-' * (80 - filled_length) - print(f'\rProgress: |{bar}| {percent}%', end="\r") + bar = "*" * filled_length + "-" * (80 - filled_length) + print(f"\rProgress: |{bar}| {percent}%", end="\r") -e = EPlusAPIHelper(Path(eplus_path)) +e = EPlusAPIHelper(get_eplus_path_from_argv1()) api = e.get_api_instance() state = api.state_manager.new_state() api.runtime.set_console_output_status(state, False) api.runtime.callback_progress(state, progress_update) result = api.runtime.run_energyplus( - state, - [ - '-d', - e.get_temp_run_dir(), - '-w', - e.weather_file_path(), - "-a", - e.path_to_test_file('5ZoneAirCooled.idf') - ] + state, ["-d", e.get_temp_run_dir(), "-w", e.weather_file_path(), "-a", e.path_to_test_file("5ZoneAirCooled.idf")] ) if result == 0: print("Success, finished") diff --git a/energyplus_api_helpers/demos/05_dynamic_terminal_output.py b/energyplus_api_helpers/demos/05_dynamic_terminal_output.py index 849c0e4..25524ab 100644 --- a/energyplus_api_helpers/demos/05_dynamic_terminal_output.py +++ b/energyplus_api_helpers/demos/05_dynamic_terminal_output.py @@ -1,54 +1,51 @@ import sys -from pathlib import Path -from sys import argv +from typing import List, Optional + import sparkline + +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper -outdoor_db_handle = None -plot_data = [] +outdoor_db_handle: Optional[int] = None +plot_data: List[float] = [] counter = 0 -eplus_path = '/eplus/installs/EnergyPlus-22-2-0' -if len(argv) > 1: - eplus_path = argv[1] - def callback_function(s): global outdoor_db_handle, counter if not outdoor_db_handle: - outdoor_db_handle = api.exchange.get_variable_handle( - s, "Site Outdoor Air Drybulb Temperature", "Environment" - ) + outdoor_db_handle = api.exchange.get_variable_handle(s, "Site Outdoor Air Drybulb Temperature", "Environment") # data = api.exchange.list_available_api_data_csv(s) # with open('/tmp/data.csv', 'wb') as f: # f.write(data) if api.exchange.warmup_flag(s) or api.exchange.current_environment_num(s) < 3: - sys.stdout.write('\r') - sys.stdout.write('Simulation Starting...') + sys.stdout.write("\r") + sys.stdout.write("Simulation Starting...") sys.stdout.flush() return counter += 1 if counter % 600 == 0: plot_data.append(api.exchange.get_variable_value(s, outdoor_db_handle)) - sys.stdout.write('\r') + sys.stdout.write("\r") sys.stdout.flush() sys.stdout.write("Outdoor Temperature: " + sparkline.sparkify(plot_data)) sys.stdout.flush() -e = EPlusAPIHelper(Path(eplus_path)) +e = EPlusAPIHelper(get_eplus_path_from_argv1()) api = e.get_api_instance() state = api.state_manager.new_state() api.runtime.set_console_output_status(state, False) api.runtime.callback_begin_zone_timestep_before_init_heat_balance(state, callback_function) api.exchange.request_variable(state, "Site Outdoor Air Drybulb Temperature", "Environment") api.runtime.run_energyplus( - state, [ - '-d', + state, + [ + "-d", e.get_temp_run_dir(), - '-w', + "-w", e.weather_file_path(), "-a", - e.path_to_test_file('5ZoneAirCooled.idf'), - ] + e.path_to_test_file("5ZoneAirCooled.idf"), + ], ) diff --git a/energyplus_api_helpers/demos/06_plot_e_plus.py b/energyplus_api_helpers/demos/06_plot_e_plus.py index 4d21b3e..50df5ba 100644 --- a/energyplus_api_helpers/demos/06_plot_e_plus.py +++ b/energyplus_api_helpers/demos/06_plot_e_plus.py @@ -1,17 +1,18 @@ import matplotlib.pyplot as plt -from pathlib import Path + +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper class PlotManager: def __init__(self): - self.hl, = plt.plot([], [], label="Outdoor Air Temp") - self.h2, = plt.plot([], [], label="Zone Temperature") + (self.hl,) = plt.plot([], [], label="Outdoor Air Temp") + (self.h2,) = plt.plot([], [], label="Zone Temperature") self.ax = plt.gca() - plt.title('Outdoor Temperature') - plt.xlabel('Zone time step index') - plt.ylabel('Temperature [C]') - plt.legend(loc='lower right') + plt.title("Outdoor Temperature") + plt.xlabel("Zone time step index") + plt.ylabel("Temperature [C]") + plt.legend(loc="lower right") self.x = [] self.y_outdoor = [] self.y_zone = [] @@ -30,7 +31,7 @@ def update_line(self): class EnergyPlusManager: def __init__(self): - self.e = EPlusAPIHelper(Path('/eplus/installs/EnergyPlus-22-2-0')) + self.e = EPlusAPIHelper(get_eplus_path_from_argv1()) self.api = self.e.get_api_instance() self.got_handles = False self.oa_temp_handle = -1 @@ -43,10 +44,10 @@ def callback_function(self, state): if not self.api.exchange.api_data_fully_ready(state): return self.oa_temp_handle = self.api.exchange.get_variable_handle( - state, u"SITE OUTDOOR AIR DRYBULB TEMPERATURE", u"ENVIRONMENT" + state, "SITE OUTDOOR AIR DRYBULB TEMPERATURE", "ENVIRONMENT" ) self.zone_temp_handle = self.api.exchange.get_variable_handle( - state, "Zone Mean Air Temperature", 'Main Zone' + state, "Zone Mean Air Temperature", "Main Zone" ) if -1 in [self.oa_temp_handle, self.zone_temp_handle]: print("***Invalid handles, check spelling and sensor/actuator availability") @@ -66,12 +67,16 @@ def callback_function(self, state): def run(self): state = self.api.state_manager.new_state() self.api.runtime.callback_begin_zone_timestep_after_init_heat_balance(state, self.callback_function) - self.api.runtime.run_energyplus(state, [ - '-a', - '-w', self.e.weather_file_path(), - '-d', self.e.get_temp_run_dir(), - self.e.path_to_test_file('1ZoneEvapCooler.idf') - ] + self.api.runtime.run_energyplus( + state, + [ + "-a", + "-w", + self.e.weather_file_path(), + "-d", + self.e.get_temp_run_dir(), + self.e.path_to_test_file("1ZoneEvapCooler.idf"), + ], ) diff --git a/energyplus_api_helpers/demos/07_server.py b/energyplus_api_helpers/demos/07_server.py index c37dd0f..70404bb 100644 --- a/energyplus_api_helpers/demos/07_server.py +++ b/energyplus_api_helpers/demos/07_server.py @@ -1,11 +1,16 @@ -from flask import Flask, request from pathlib import Path from threading import Thread from time import sleep +from typing import Dict, List + +from flask import Flask, request + +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper app = Flask("EnergyPlus API Server Demo") -e = EPlusAPIHelper(Path('/eplus/installs/EnergyPlus-22-2-0')) +e = EPlusAPIHelper(get_eplus_path_from_argv1()) + api = e.get_api_instance() eplus_outdoor_temp = 23.3 @@ -16,47 +21,47 @@ oa_temp_handle = -1 zone_temp_handle = -1 count = 0 -outdoor_data = [] -zone_temp_data = [] +outdoor_data: List[Dict[str, float]] = [] +zone_temp_data: List[Dict[str, float]] = [] -@app.route("/", methods=['GET']) +@app.route("/", methods=["GET"]) def hello(): - html_file = Path(__file__).resolve().parent / '07_server_index.html' + html_file = Path(__file__).resolve().parent / "07_server_index.html" return html_file.read_text() -@app.route('/api/data/', methods=['GET']) +@app.route("/api/data/", methods=["GET"]) def get_api_data(): return { - "output": eplus_output.decode('utf-8'), + "output": eplus_output.decode("utf-8"), "progress": eplus_progress + 1, "outdoor_data": outdoor_data, - "zone_temp_data": zone_temp_data + "zone_temp_data": zone_temp_data, } -@app.route('/api/start/', methods=['POST']) +@app.route("/api/start/", methods=["POST"]) def post_api_start(): Thread(target=thread_function).start() return {} -@app.route('/api/outdoor_temp/', methods=['POST']) +@app.route("/api/outdoor_temp/", methods=["POST"]) def get_outdoor_temp(): global eplus_outdoor_temp data = request.json print(data) - if 'temperature' not in data: + if "temperature" not in data: return {"message": "Need to supply 'temperature' in POST data as a float"} - temp = float(data['temperature']) + temp = float(data["temperature"]) eplus_outdoor_temp = temp return {"outdoor_temp": eplus_outdoor_temp} def eplus_output_handler(msg): global eplus_output - eplus_output += msg + b'\n' + eplus_output += msg + b"\n" def eplus_progress_handler(p): @@ -71,7 +76,7 @@ def callback_function(s): return oa_temp_actuator = api.exchange.get_actuator_handle(s, "Weather Data", "Outdoor Dry Bulb", "Environment") oa_temp_handle = api.exchange.get_variable_handle(s, "Site Outdoor Air DryBulb Temperature", "Environment") - zone_temp_handle = api.exchange.get_variable_handle(s, "Zone Mean Air Temperature", 'Zone One') + zone_temp_handle = api.exchange.get_variable_handle(s, "Zone Mean Air Temperature", "Zone One") if -1 in [oa_temp_actuator, oa_temp_handle, zone_temp_handle]: print("***Invalid handles, check spelling and sensor/actuator availability") # TODO: Ask E+ to fatal error @@ -84,9 +89,9 @@ def callback_function(s): return api.exchange.set_actuator_value(s, oa_temp_actuator, eplus_outdoor_temp) oa_temp = api.exchange.get_variable_value(s, oa_temp_handle) - outdoor_data.append({'x': count, 'y': oa_temp}) + outdoor_data.append({"x": count, "y": oa_temp}) zone_temp = api.exchange.get_variable_value(s, zone_temp_handle) - zone_temp_data.append({'x': count, 'y': zone_temp}) + zone_temp_data.append({"x": count, "y": zone_temp}) def thread_function(): @@ -98,19 +103,13 @@ def thread_function(): zone_temp_data = [] state = api.state_manager.new_state() api.exchange.request_variable(state, "Site Outdoor Air DryBulb Temperature", "Environment") - api.exchange.request_variable(state, "Zone Mean Air Temperature", 'Zone One') + api.exchange.request_variable(state, "Zone Mean Air Temperature", "Zone One") api.runtime.callback_begin_zone_timestep_after_init_heat_balance(state, callback_function) api.runtime.callback_message(state, eplus_output_handler) api.runtime.callback_progress(state, eplus_progress_handler) api.runtime.run_energyplus( - state, [ - '-d', - e.get_temp_run_dir(), - '-a', - '-w', - e.weather_file_path(), - e.path_to_test_file('1ZoneUncontrolled.idf') - ] + state, + ["-d", e.get_temp_run_dir(), "-a", "-w", e.weather_file_path(), e.path_to_test_file("1ZoneUncontrolled.idf")], ) diff --git a/energyplus_api_helpers/demos/08_server_advanced.py b/energyplus_api_helpers/demos/08_server_advanced.py index 8eb9b7e..e046bcd 100644 --- a/energyplus_api_helpers/demos/08_server_advanced.py +++ b/energyplus_api_helpers/demos/08_server_advanced.py @@ -1,14 +1,17 @@ -from flask import Flask from pathlib import Path from threading import Thread from time import sleep + +from flask import Flask + +from energyplus_api_helpers.demos.helper import get_eplus_path_from_argv1 from energyplus_api_helpers.import_helper import EPlusAPIHelper class RunConfig: def __init__(self): - self.e = EPlusAPIHelper(Path('/eplus/installs/EnergyPlus-22-2-0')) - self.idf_name = '5ZoneAirCooled.idf' + self.e = EPlusAPIHelper(get_eplus_path_from_argv1()) + self.idf_name = "5ZoneAirCooled.idf" self.api = self.e.get_api_instance() self.eplus_outdoor_temp = 23.3 self.eplus_output = b"HERE IA M" @@ -20,8 +23,11 @@ def __init__(self): self.count = 0 self.outdoor_data = [] self.zone_names = { - 'south': 'SPACE1-1', 'west': 'SPACE2-1', 'east': 'SPACE3-1', - 'north': 'SPACE4-1', 'center': 'SPACE5-1' + "south": "SPACE1-1", + "west": "SPACE2-1", + "east": "SPACE3-1", + "north": "SPACE4-1", + "center": "SPACE5-1", } @@ -29,24 +35,24 @@ def __init__(self): app = Flask("EnergyPlus API Server Demo") -@app.route("/", methods=['GET']) +@app.route("/", methods=["GET"]) def hello(): - html_file = Path(__file__).resolve().parent / '08_server_advanced_index.html' + html_file = Path(__file__).resolve().parent / "08_server_advanced_index.html" return html_file.read_text() -@app.route('/api/data/', methods=['GET']) +@app.route("/api/data/", methods=["GET"]) def get_api_data(): global runner return { - "output": runner.eplus_output.decode('utf-8'), + "output": runner.eplus_output.decode("utf-8"), "progress": runner.eplus_progress + 1, "outdoor_data": runner.outdoor_data, - "zone_temp_data": runner.zone_temperatures + "zone_temp_data": runner.zone_temperatures, } -@app.route('/api/start/', methods=['POST']) +@app.route("/api/start/", methods=["POST"]) def post_api_start(): Thread(target=thread_function).start() return {} @@ -54,7 +60,7 @@ def post_api_start(): def eplus_output_handler(msg): global runner - runner.eplus_output += msg + b'\n' + runner.eplus_output += msg + b"\n" def eplus_progress_handler(p): @@ -72,7 +78,7 @@ def callback_function(s): ) for zone_nickname, zone_name in runner.zone_names.items(): runner.zone_temp_handles[zone_nickname] = runner.api.exchange.get_variable_handle( - s, u"ZONE AIR TEMPERATURE", zone_name + s, "ZONE AIR TEMPERATURE", zone_name ) if -1 in [runner.oa_temp_handle] + list(runner.zone_temp_handles.values()): runner.api.runtime.issue_severe("Invalid Handle in API usage, need to fix!") @@ -84,7 +90,7 @@ def callback_function(s): if runner.count % 200 != 0: return oa_temp = runner.api.exchange.get_variable_value(s, runner.oa_temp_handle) - runner.outdoor_data.append({'x': runner.count, 'y': oa_temp}) + runner.outdoor_data.append({"x": runner.count, "y": oa_temp}) for zone_nickname in runner.zone_names: runner.zone_temperatures[zone_nickname] = runner.api.exchange.get_variable_value( s, runner.zone_temp_handles[zone_nickname] @@ -103,14 +109,15 @@ def thread_function(): runner.api.runtime.callback_progress(state, eplus_progress_handler) runner.api.runtime.set_console_output_status(state, False) runner.api.runtime.run_energyplus( - state, [ - '-d', + state, + [ + "-d", runner.e.get_temp_run_dir(), - '-a', - '-w', + "-a", + "-w", runner.e.weather_file_path(), - runner.e.path_to_test_file(runner.idf_name) - ] + runner.e.path_to_test_file(runner.idf_name), + ], ) diff --git a/energyplus_api_helpers/demos/helper.py b/energyplus_api_helpers/demos/helper.py new file mode 100644 index 0000000..42c7e84 --- /dev/null +++ b/energyplus_api_helpers/demos/helper.py @@ -0,0 +1,10 @@ +import sys +from pathlib import Path +from typing import Optional + + +def get_eplus_path_from_argv1() -> Optional[Path]: + """If there is one argv, get the installation from it, otherwise leave it be inferred.""" + if len(sys.argv) > 1: + return Path(sys.argv[1]) + return None diff --git a/energyplus_api_helpers/import_helper.py b/energyplus_api_helpers/import_helper.py index d2a274f..3bdf3fc 100644 --- a/energyplus_api_helpers/import_helper.py +++ b/energyplus_api_helpers/import_helper.py @@ -1,6 +1,37 @@ -from pathlib import Path +import platform +import shutil import sys +from pathlib import Path from tempfile import mkdtemp +from typing import Optional + + +def _infer_energyplus_install_dir(): + """Try to locate the EnergyPlus installation path. + + Starts by looking at `which energyplus` first then tries the default installation paths. + Returns the most recent version found""" + if ep := shutil.which("energyplus"): + return Path(ep).resolve().parent + ext = "" + if platform.system() == 'Linux': + base_dir = Path('/usr/local') + elif platform.system() == 'Darwin': + base_dir = Path('/Applications') + else: + base_dir = Path('C:/') + ext = ".exe" + if not base_dir.is_dir(): + raise ValueError(f"{base_dir=} is not a directory") + candidates = [p.parent for p in base_dir.glob(f"EnergyPlus*/energyplus{ext}")] + if not candidates: + raise ValueError("Found zero EnergyPlus installation directories") + candidates = [c for c in candidates if (c / 'pyenergyplus').is_dir()] + if not candidates: + raise ValueError("Found zero EnergyPlus installation directories that have the pyenergyplus directory") + # Sort by version + candidates.sort(key=lambda c: [int(x) for x in c.name.split('-')[1:]]) + return candidates[-1] class _EPlusImporter: @@ -19,6 +50,7 @@ class _EPlusImporter: api = EnergyPlusAPI() """ + def __init__(self, eplus_install_path: Path): self.eplus_install_path = eplus_install_path @@ -37,43 +69,56 @@ class EPlusAPIHelper: This is the primary helper class for providing usability to E+ API clients This class intentionally provides strings as path outputs to keep the conversion to strings reduced in the client """ - def __init__(self, eplus_install_path: Path): - self.eplus_install_path = eplus_install_path + + def __init__(self, eplus_install_path: Optional[Path] = None): + if eplus_install_path is None: + self.eplus_install_path = _infer_energyplus_install_dir() + print(f"Infered Location of EnergyPlus installation at {self.eplus_install_path}") + else: + if not isinstance(eplus_install_path, Path): + eplus_install_path = Path(eplus_install_path) + if not (eplus_install_path / 'pyenergyplus').is_dir(): + raise ValueError(f"Wrong eplus_install_path, '{eplus_install_path}/pyenergyplus' does not exist") + self.eplus_install_path = eplus_install_path def get_api_instance(self): with _EPlusImporter(self.eplus_install_path): from pyenergyplus.api import EnergyPlusAPI + return EnergyPlusAPI() def is_an_install_folder(self) -> bool: - if (self.eplus_install_path / 'ExampleFiles').exists(): + if (self.eplus_install_path / "ExampleFiles").exists(): return True return False def find_source_dir_from_cmake_cache(self) -> Path: - cmake_cache_file = self.eplus_install_path.parent / 'CMakeCache.txt' - lines = cmake_cache_file.read_text().split('\n') + cmake_cache_file = self.eplus_install_path.parent / "CMakeCache.txt" + if not cmake_cache_file.is_file(): + raise ValueError("Cannot locate the CMakeCache.txt") + lines = cmake_cache_file.read_text().split("\n") for line in lines: line_trimmed = line.strip() - if line_trimmed.startswith('EnergyPlus_SOURCE_DIR:STATIC='): - found_dir = line_trimmed.split('=')[1] + if line_trimmed.startswith("EnergyPlus_SOURCE_DIR:STATIC="): + found_dir = line_trimmed.split("=")[1] return Path(found_dir) + raise ValueError(f"Could not locate the source directory in {cmake_cache_file}") def path_to_test_file(self, test_file_name: str) -> str: """Returns the path to an example/test file, trying to figure out if it is a build dir or install.""" if self.is_an_install_folder(): - return str(self.eplus_install_path / 'ExampleFiles' / test_file_name) + return str(self.eplus_install_path / "ExampleFiles" / test_file_name) else: source_dir = self.find_source_dir_from_cmake_cache() - return str(source_dir / 'testfiles' / test_file_name) + return str(source_dir / "testfiles" / test_file_name) - def weather_file_path(self, weather_file_name: str = 'USA_IL_Chicago-OHare.Intl.AP.725300_TMY3.epw') -> str: + def weather_file_path(self, weather_file_name: str = "USA_IL_Chicago-OHare.Intl.AP.725300_TMY3.epw") -> str: """Gets a path to a default weather file""" if self.is_an_install_folder(): - return str(self.eplus_install_path / 'WeatherData' / weather_file_name) + return str(self.eplus_install_path / "WeatherData" / weather_file_name) else: source_dir = self.find_source_dir_from_cmake_cache() - return str(source_dir / 'weather' / weather_file_name) + return str(source_dir / "weather" / weather_file_name) @staticmethod def get_temp_run_dir() -> str: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e5b8022 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "energyplus_api_helpers" +version = "0.4.0" +description = "A set of helper classes, functions and demos, for interacting with the EnergyPlus Python API" +readme = "README.md" +requires-python = ">=3.7" +keywords = [ + "energyplus_launch", "ep_launch", + "EnergyPlus", "eplus", "Energy+", + "Building Simulation", "Whole Building Energy Simulation", + "Heat Transfer", "HVAC", "Modeling", +] +license = {file = "License.txt"} +authors = [ + {name = 'Edwin Lee, for NREL, for the United States Department of Energy'}, +] +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Utilities", +] +dependencies = [ + "matplotlib", "flask", "pysparklines", "asciichartpy" +] + + +[tool.setuptools] +packages = ["energyplus_api_helpers", "energyplus_api_helpers.demos"] + +[tool.setuptools.package-data] +energyplus_api_helpers= ["*.html"] + +[project.urls] +homepage = "https://github.com/Myoldmopar/EnergyPlusAPIHelper" +#documentation = "https://docs.scipy.org/doc/scipy/" +source = "https://github.com/Myoldmopar/EnergyPlusAPIHelper" +#download = "https://github.com/scipy/scipy/releases" +tracker = "https://github.com/Myoldmopar/EnergyPlusAPIHelper/issues" + +[project.optional-dependencies] +dev = ["black", "isort", "mypy", "build", "pre-commit"] + +[tool.black] +line-length = 120 +skip-string-normalization = true +target-version = ['py37', 'py38', 'py39'] +include = '\.pyi?$' +exclude = ''' +/( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.isort] +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 120 +skip_gitignore = true + +[tool.mypy] +ignore_missing_imports = "True" +check_untyped_defs = "True" diff --git a/requirements.txt b/requirements.txt index 66a893b..bf61fb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ matplotlib pysparklines asciichartpy flask -wheel \ No newline at end of file +wheel diff --git a/setup.py b/setup.py deleted file mode 100644 index 59f1c19..0000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -import pathlib -from setuptools import setup - -readme_file = pathlib.Path(__file__).parent.resolve() / 'README.md' -readme_contents = readme_file.read_text() - -setup( - name="energyplus_api_helpers", - version="0.4", - packages=['energyplus_api_helpers', 'energyplus_api_helpers.demos'], - description="A set of helper classes, functions and demos, for interacting with the EnergyPlus Python API", - package_data={"energyplus_api_helpers.demos": ["*.html"]}, - include_package_data=True, - long_description=readme_contents, - long_description_content_type='text/markdown', - author='Edwin Lee, for NREL, for the United States Department of Energy', - url='https://github.com/Myoldmopar/EnergyPlusAPIHelper', - license='ModifiedBSD', - install_requires=['matplotlib', 'flask', 'pysparklines', 'asciichartpy'], - # entry_points={ - # 'console_scripts': ['energyplus_api_helper=energyplus_api_helpers.runner:main_gui'] - # } - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Science/Research', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3 :: Only', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Physics', - 'Topic :: Utilities', - ], - platforms=[ - 'Linux (Tested on Ubuntu)', 'MacOSX', 'Windows' - ], - keywords=[ - 'energyplus_launch', 'ep_launch', - 'EnergyPlus', 'eplus', 'Energy+', - 'Building Simulation', 'Whole Building Energy Simulation', - 'Heat Transfer', 'HVAC', 'Modeling', - ] -)