-
Notifications
You must be signed in to change notification settings - Fork 61
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
934 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# https://github.com/actions/starter-workflows/blob/main/ci/python-package.yml | ||
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions | ||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python | ||
|
||
name: Python package | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
pull_request: | ||
branches: [ main ] | ||
workflow_dispatch: | ||
|
||
jobs: | ||
build: | ||
|
||
runs-on: ubuntu-latest | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
python -m pip install . | ||
python -m pip install flake8 pytest ruff | ||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi | ||
- name: Lint with flake8 | ||
run: | | ||
# stop the build if there are Python syntax errors or undefined names | ||
# exit-zero treats all errors as warnings. | ||
flake8 . --count --exit-zero --show-source --statistics | ||
- name: Lint with ruff | ||
run: | | ||
ruff . | ||
- name: Test with pytest | ||
run: | | ||
pytest |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# https://github.com/actions/starter-workflows/blob/main/ci/python-publish.yml | ||
# This workflow will upload a Python Package using Twine when a release is created | ||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries | ||
|
||
# This workflow uses actions that are not certified by GitHub. | ||
# They are provided by a third-party and are governed by | ||
# separate terms of service, privacy policy, and support | ||
# documentation. | ||
|
||
name: Upload Python Package | ||
|
||
on: | ||
release: | ||
types: [published] | ||
|
||
permissions: | ||
contents: read | ||
|
||
jobs: | ||
deploy: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Set up Python | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: '3.x' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install build | ||
- name: Build package | ||
run: python -m build | ||
- name: Publish package | ||
uses: pypa/gh-action-pypi-publish@release/v1 | ||
with: | ||
user: __token__ | ||
password: ${{ secrets.PYPI_API_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,46 @@ | ||
# opower | ||
Python library to get historical and forecasted usage/cost from utilities that use opower.com such as PG&E | ||
A Python library for getting historical and forecasted usage/cost from utilities that use opower.com such as PG&E. | ||
|
||
To add support for a new utility that uses opower JSON API (you can tell if the energy dashboard of your utility is hosted on opower.com, e.g. pge.opower.com) add a file similar to [pge.py](https://github.com/tronikos/opower/blob/main/src/opower/utilities/pge.py). | ||
|
||
## Example | ||
|
||
See [demo.py](https://github.com/tronikos/opower/blob/main/src/demo.py) | ||
|
||
## Development environment | ||
|
||
```sh | ||
python3 -m venv .venv | ||
source .venv/bin/activate | ||
# for Windows CMD: | ||
# .venv\Scripts\activate.bat | ||
# for Windows PowerShell: | ||
# .venv\Scripts\Activate.ps1 | ||
|
||
# Install dependencies | ||
python -m pip install --upgrade pip | ||
python -m pip install . | ||
|
||
# Run formatter | ||
python -m pip install isort black | ||
isort . | ||
black . | ||
|
||
# Run lint | ||
python -m pip install flake8 ruff | ||
flake8 . | ||
ruff . | ||
|
||
# Run tests | ||
python -m pip install pytest | ||
pytest | ||
|
||
# Run demo | ||
python src/demo.py --help | ||
# To output debug logs to a file: | ||
python src/demo.py --verbose 2> out.txt | ||
|
||
# Build package | ||
python -m pip install build | ||
python -m build | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
[project] | ||
name = "opower" | ||
version = "0.0.1" | ||
license = {text = "Apache-2.0"} | ||
authors = [ | ||
{ name="tronikos", email="[email protected]" }, | ||
] | ||
description = "A Python library for getting historical and forecasted usage/cost from utilities that use opower.com such as PG&E" | ||
readme = "README.md" | ||
requires-python = ">=3.7" | ||
dependencies = [ | ||
"aiohttp>=3.8", | ||
"asyncio>=3.4.3", | ||
] | ||
|
||
[project.urls] | ||
"Homepage" = "https://github.com/tronikos/opower" | ||
"Bug Tracker" = "https://github.com/tronikos/opower/issues" | ||
|
||
[build-system] | ||
requires = ["setuptools"] | ||
build-backend = "setuptools.build_meta" | ||
|
||
[tool.black] | ||
extend-exclude = "_pb2.py|_pb2_grpc.py" | ||
|
||
[tool.isort] | ||
profile = "black" | ||
force_sort_within_sections = true | ||
combine_as_imports = true | ||
extend_skip_glob = ["*_pb2.py", "*_pb2_grpc.py"] | ||
|
||
[tool.ruff] | ||
target-version = "py311" | ||
exclude = ["*_pb2.py", "*_pb2_grpc.py", "*.pyi"] | ||
line-length = 127 | ||
|
||
select = [ | ||
"B007", # Loop control variable {name} not used within loop body | ||
"B014", # Exception handler with duplicate exception | ||
"C", # complexity | ||
"D", # docstrings | ||
"E", # pycodestyle | ||
"F", # pyflakes/autoflake | ||
"ICN001", # import concentions; {name} should be imported as {asname} | ||
"PGH004", # Use specific rule codes when using noqa | ||
"PLC0414", # Useless import alias. Import alias does not rename original package. | ||
"SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass | ||
"SIM117", # Merge with-statements that use the same scope | ||
"SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() | ||
"SIM201", # Use {left} != {right} instead of not {left} == {right} | ||
"SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} | ||
"SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. | ||
"SIM401", # Use get from dict with default instead of an if block | ||
"T20", # flake8-print | ||
"TRY004", # Prefer TypeError exception for invalid type | ||
"RUF006", # Store a reference to the return value of asyncio.create_task | ||
"UP", # pyupgrade | ||
"W", # pycodestyle | ||
] | ||
|
||
ignore = [ | ||
"D203", # 1 blank line required before class docstring | ||
"D213", # Multi-line docstring summary should start at the second line | ||
] | ||
|
||
[tool.ruff.flake8-pytest-style] | ||
fixture-parentheses = false | ||
|
||
[tool.ruff.pyupgrade] | ||
keep-runtime-typing = true | ||
|
||
[tool.ruff.per-file-ignores] | ||
# Allow for demo script to write to stdout | ||
"demo.py" = ["T201"] | ||
|
||
[tool.ruff.mccabe] | ||
max-complexity = 25 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
[flake8] | ||
exclude = .venv,.git,docs,venv,bin,lib,deps,build,*_pb2.py,*_pb2_grpc.py | ||
max-complexity = 25 | ||
doctests = True | ||
# To work with Black | ||
# E501: line too long | ||
# W503: Line break occurred before a binary operator | ||
# E203: Whitespace before ':' | ||
# D202 No blank lines allowed after function docstring | ||
# W504 line break after binary operator | ||
ignore = | ||
E501, | ||
W503, | ||
E203, | ||
D202, | ||
W504 | ||
noqa-require-code = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
"""Demo usage of Opower library.""" | ||
|
||
import argparse | ||
import asyncio | ||
from datetime import datetime, timedelta | ||
from getpass import getpass | ||
import logging | ||
|
||
import aiohttp | ||
|
||
from opower import AggregateType, Opower, get_supported_utility_subdomains | ||
|
||
|
||
async def _main(): | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument( | ||
"--utility", | ||
help="Utility (subdomain of opower.com). Defaults to pge", | ||
choices=get_supported_utility_subdomains(), | ||
default="pge", | ||
) | ||
parser.add_argument( | ||
"--username", | ||
help="Username for logging into the utility's website. " | ||
"If not provided, you will be asked for it", | ||
) | ||
parser.add_argument( | ||
"--password", | ||
help="Password for logging into the utility's website. " | ||
"If not provided, you will be asked for it", | ||
) | ||
parser.add_argument( | ||
"--aggregate_type", | ||
help="How to aggregate historical data. Defaults to day", | ||
choices=list(AggregateType), | ||
default=AggregateType.DAY, | ||
) | ||
parser.add_argument( | ||
"--start_date", | ||
help="Start datetime for historical data. Defaults to 7 days ago", | ||
type=lambda s: datetime.fromisoformat(s), | ||
default=datetime.now() - timedelta(days=7), | ||
) | ||
parser.add_argument( | ||
"--end_date", | ||
help="end datetime for historical data. Defaults to now", | ||
type=lambda s: datetime.fromisoformat(s), | ||
default=datetime.now(), | ||
) | ||
parser.add_argument( | ||
"--usage_only", | ||
help="If true will output usage only, not cost", | ||
action="store_true", | ||
) | ||
parser.add_argument( | ||
"-v", "--verbose", help="enable verbose logging", action="store_true" | ||
) | ||
args = parser.parse_args() | ||
|
||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) | ||
|
||
username = args.username or input("Username: ") | ||
password = args.password or getpass("Password: ") | ||
|
||
async with aiohttp.ClientSession() as session: | ||
opower = Opower(session, args.utility, username, password) | ||
await opower.async_login() | ||
forecasts = await opower.async_get_forecast() | ||
for forecast in forecasts: | ||
print("\nData for meter:", forecast.account.meter_type) | ||
print("\nCurrent bill forecast:", forecast) | ||
print( | ||
"\nGetting historical data: aggregate_type=", | ||
args.aggregate_type, | ||
"start_date=", | ||
args.start_date, | ||
"end_date=", | ||
args.end_date, | ||
) | ||
if args.usage_only: | ||
usage_data = await opower.async_get_usage_reads( | ||
forecast.account, | ||
args.aggregate_type, | ||
args.start_date, | ||
args.end_date, | ||
) | ||
prev_end = None | ||
print( | ||
"start_time\tend_time\tconsumption" | ||
"\tstart_minus_prev_end\tend_minus_prev_end" | ||
) | ||
for usage_read in usage_data: | ||
start_minus_prev_end = ( | ||
None if prev_end is None else usage_read.start_time - prev_end | ||
) | ||
end_minus_prev_end = ( | ||
None if prev_end is None else usage_read.end_time - prev_end | ||
) | ||
prev_end = usage_read.end_time | ||
print( | ||
f"{usage_read.start_time}" | ||
f"\t{usage_read.end_time}" | ||
f"\t{usage_read.consumption}" | ||
f"\t{start_minus_prev_end}" | ||
f"\t{end_minus_prev_end}" | ||
) | ||
else: | ||
cost_data = await opower.async_get_cost_reads( | ||
forecast.account, | ||
args.aggregate_type, | ||
args.start_date, | ||
args.end_date, | ||
) | ||
prev_end = None | ||
print( | ||
"start_time\tend_time\tconsumption\tprovided_cost" | ||
"\tstart_minus_prev_end\tend_minus_prev_end" | ||
) | ||
for cost_read in cost_data: | ||
start_minus_prev_end = ( | ||
None if prev_end is None else cost_read.start_time - prev_end | ||
) | ||
end_minus_prev_end = ( | ||
None if prev_end is None else cost_read.end_time - prev_end | ||
) | ||
prev_end = cost_read.end_time | ||
print( | ||
f"{cost_read.start_time}" | ||
f"\t{cost_read.end_time}" | ||
f"\t{cost_read.consumption}" | ||
f"\t{cost_read.provided_cost}" | ||
f"\t{start_minus_prev_end}" | ||
f"\t{end_minus_prev_end}" | ||
) | ||
print() | ||
|
||
|
||
asyncio.run(_main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
"""Library for getting historical and forecasted usage/cost from an utility using opower.com JSON API.""" | ||
|
||
from .opower import ( | ||
Account, | ||
AggregateType, | ||
CostRead, | ||
Forecast, | ||
MeterType, | ||
Opower, | ||
UnitOfMeasure, | ||
UsageRead, | ||
get_supported_utility_names, | ||
get_supported_utility_subdomains, | ||
) | ||
|
||
__all__ = [ | ||
"Account", | ||
"AggregateType", | ||
"CostRead", | ||
"Forecast", | ||
"MeterType", | ||
"Opower", | ||
"UnitOfMeasure", | ||
"UsageRead", | ||
"get_supported_utility_names", | ||
"get_supported_utility_subdomains", | ||
] |
Oops, something went wrong.