pax_global_header00006660000000000000000000000064147517017710014524gustar00rootroot0000000000000052 comment=c8a0d0173d03bfc53ffb1b98d51c98c472af76eb pyrympro-0.0.9/000077500000000000000000000000001475170177100134335ustar00rootroot00000000000000pyrympro-0.0.9/.gitignore000066400000000000000000000000541475170177100154220ustar00rootroot00000000000000__pycache__/ *.pyc build/ dist/ *.egg-info/pyrympro-0.0.9/LICENSE000066400000000000000000000020511475170177100144360ustar00rootroot00000000000000MIT License Copyright (c) 2022 On Freund Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.pyrympro-0.0.9/MANIFEST.in000066400000000000000000000000311475170177100151630ustar00rootroot00000000000000include README.md LICENSEpyrympro-0.0.9/README.md000066400000000000000000000010541475170177100147120ustar00rootroot00000000000000# pyrympro A python library to communitcate with [Read Your Meter Pro](https://rym-pro.com/). ## Installation You can install pyvolumio from [PyPI](https://pypi.org/project/pyvolumio/): pip3 install pyrympro Python 3.7 and above are supported. ## How to use ```python from pyrympro import RymPro rym = RymPro() # you can also pass in your own session rym = RymPro(session) # device_id can be anything you choose await rym.login("", "", "") info = await rym.account_info() meter_reads = await rym.last_read() ... ```pyrympro-0.0.9/pyrympro/000077500000000000000000000000001475170177100153345ustar00rootroot00000000000000pyrympro-0.0.9/pyrympro/__init__.py000066400000000000000000000001221475170177100174400ustar00rootroot00000000000000from .rympro import CannotConnectError, OperationError, UnauthorizedError, RymPro pyrympro-0.0.9/pyrympro/const.py000066400000000000000000000010631475170177100170340ustar00rootroot00000000000000from enum import Enum BASE_URL = "https://eu-customerportal-api.harmonyencoremdm.com" CONSUMER_URL = f"{BASE_URL}/consumer" CONSUMPTION_URL = f"{BASE_URL}/consumption" class Endpoint(Enum): LOGIN = f"{CONSUMER_URL}/login" ACCOUNT_INFO = f"{CONSUMER_URL}/me" LAST_READ = f"{CONSUMPTION_URL}/last-read" CONSUMPTION_FORECAST = f"{CONSUMPTION_URL}/forecast/{{meter_id}}" DAILY_CONSUMPTION = f"{CONSUMPTION_URL}/daily/{{meter_id}}/{{start_date}}/{{end_date}}" MONTHLY_CONSUMPTION = f"{CONSUMPTION_URL}/monthly/{{meter_id}}/{{start_date}}/{{end_date}}" pyrympro-0.0.9/pyrympro/rympro.py000066400000000000000000000107601475170177100172420ustar00rootroot00000000000000"""Implementation of a RymPro inteface.""" import aiohttp import asyncio from datetime import datetime from typing import Any, Optional from .const import Endpoint class RymPro: """A connection to RYM Pro.""" def __init__(self, session: Optional[aiohttp.ClientSession] = None) -> None: """Initialize the object.""" self._created_session = False self._session = session self._access_token: Optional[str] = None async def close(self) -> None: """Close the connection.""" if self._created_session and self._session is not None: await self._session.close() self._session = None self._created_session = False def set_token(self, token: str) -> None: """Use a pre-existing token instead of logging in.""" self._init_session() self._access_token = token async def login(self, email: str, password: str, device_id: str) -> str: """Login to RYM Pro and return a token.""" self._init_session() headers = {"Content-Type": "application/json"} body = {"email": email, "pw": password, "deviceId": device_id} assert self._session try: async with self._session.post( Endpoint.LOGIN.value, headers=headers, json=body ) as response: json = await response.json() token = json.get("token") error_code = json.get("code") error = json.get("error") if error_code == 5060: raise UnauthorizedError(error) elif token is None or error_code: raise CannotConnectError(f"code: {error_code}, error: {error}") else: self._access_token = token return token except aiohttp.client_exceptions.ClientConnectorError as e: raise CannotConnectError from e async def account_info(self) -> dict[str, Any]: """Get the account information.""" return await self._get(Endpoint.ACCOUNT_INFO) async def last_read(self) -> dict[int, dict[str, Any]]: """Get the latest meter reads.""" raw: list[dict[str, Any]] = await self._get(Endpoint.LAST_READ) return { meter["meterCount"]: meter for meter in raw } async def consumption_forecast(self, meter_id: int) -> float: """Get the consumption forecast for this month.""" return (await self._get(Endpoint.CONSUMPTION_FORECAST, meter_id=meter_id))["estimatedConsumption"] async def daily_consumption(self, meter_id: int) -> float: """Get the consumption for today.""" today = datetime.now().strftime("%Y-%m-%d") result = await self._get(Endpoint.DAILY_CONSUMPTION, meter_id=meter_id, start_date=today, end_date=today) return result[0]["cons"] async def monthly_consumption(self, meter_id: int) -> float: """Get the consumption this month.""" today = datetime.now().strftime("%Y-%m-%d") result = await self._get(Endpoint.MONTHLY_CONSUMPTION, meter_id=meter_id, start_date=today, end_date=today) return result[0]["cons"] async def _get(self, endpoint: Endpoint, **kwargs: Any) -> Any: if not self._access_token: raise OperationError("Please login") headers = {"Content-Type": "application/json", "x-access-token": self._access_token} assert self._session try: async with self._session.get(endpoint.value.format(**kwargs), headers=headers) as response: if response.status == 200: return await response.json() elif response.status == 401: raise UnauthorizedError(response) else: raise OperationError(response) except (asyncio.TimeoutError, aiohttp.ClientError) as error: raise OperationError() from error def _init_session(self) -> None: if self._session == None: self._session = aiohttp.ClientSession() self._created_session = True # async def _post(self, endpoint, json_payload=None): # if not self._access_token: # raise OperationError("Please login") # headers = {"Content-Type": "application/json", "x-access-token": self._access_token} # try: # async with self._session.post(endpoint.value, headers=headers, json=json_payload) as response: # if response.status == 200: # return await response.json() # else: # raise OperationError(response) # except (asyncio.TimeoutError, aiohttp.ClientError) as error: # raise OperationError() from error class CannotConnectError(Exception): """Exception to indicate an error in connection.""" class UnauthorizedError(Exception): """Exception to indicate an error in authorization.""" class OperationError(Exception): """Exception to indicate an error in operation.""" pyrympro-0.0.9/setup.py000066400000000000000000000065261475170177100151560ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Note: To use the 'upload' functionality of this file, you must: # $ pipenv install twine --dev import io import os import sys from shutil import rmtree from setuptools import find_packages, setup, Command # Package meta-data. NAME = 'pyrympro' DESCRIPTION = 'A python library to communitcate with Read Your Meter Pro (https://rym-pro.com/).' URL = 'https://github.com/OnFreund/pyrympro' EMAIL = 'onfreund@gmail.com' AUTHOR = 'On Freund' REQUIRES_PYTHON = '>=3.10.0' VERSION = '0.0.9' REQUIRED = ['aiohttp'] EXTRAS = {} here = os.path.abspath(os.path.dirname(__file__)) # Import the README and use it as the long-description. # Note: this will only work if 'README.md' is present in your MANIFEST.in file! try: with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: long_description = '\n' + f.read() except FileNotFoundError: long_description = DESCRIPTION # Load the package's __version__.py module as a dictionary. about = {} if not VERSION: project_slug = NAME.lower().replace("-", "_").replace(" ", "_") with open(os.path.join(here, project_slug, '__version__.py')) as f: exec(f.read(), about) else: about['__version__'] = VERSION class UploadCommand(Command): """Support setup.py upload.""" description = 'Build and publish the package.' user_options = [] @staticmethod def status(s): """Prints things in bold.""" print('\033[1m{0}\033[0m'.format(s)) def initialize_options(self): pass def finalize_options(self): pass def run(self): try: self.status('Removing previous builds…') rmtree(os.path.join(here, 'dist')) except OSError: pass self.status('Building Source and Wheel distribution…') os.system('{0} setup.py sdist bdist_wheel'.format(sys.executable)) self.status('Uploading the package to PyPI via Twine…') os.system('twine upload dist/*') self.status('Pushing git tags…') os.system('git tag v{0}'.format(about['__version__'])) os.system('git push --tags') sys.exit() # Where the magic happens: setup( name=NAME, version=about['__version__'], description=DESCRIPTION, long_description=long_description, long_description_content_type='text/markdown', author=AUTHOR, author_email=EMAIL, python_requires=REQUIRES_PYTHON, url=URL, packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), # If your package is a single module, use this instead of 'packages': # py_modules=['mypackage'], # entry_points={ # 'console_scripts': ['mycli=mymodule:cli'], # }, install_requires=REQUIRED, extras_require=EXTRAS, include_package_data=True, license='MIT', classifiers=[ # Trove classifiers # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy' ], # $ setup.py publish support. cmdclass={ 'upload': UploadCommand, }, )