pax_global_header00006660000000000000000000000064147152322250014515gustar00rootroot0000000000000052 comment=58a71f061bdb7dbf06351f55ee246ed4ef1c2a52 pyplaato-0.0.19/000077500000000000000000000000001471523222500134355ustar00rootroot00000000000000pyplaato-0.0.19/.github/000077500000000000000000000000001471523222500147755ustar00rootroot00000000000000pyplaato-0.0.19/.github/release-drafter.yml000066400000000000000000000013461471523222500205710ustar00rootroot00000000000000name-template: 'v$RESOLVED_VERSION 🌈' tag-template: 'v$RESOLVED_VERSION' change-template: '- #$NUMBER $TITLE @$AUTHOR' sort-direction: ascending categories: - title: '🚀 Features' labels: - 'feature' - 'enhancement' - title: '🐛 Bug Fixes' labels: - 'fix' - 'bugfix' - 'bug' - title: '🧰 Maintenance' label: 'chore' version-resolver: major: labels: - 'major' minor: labels: - 'minor' patch: labels: - 'patch' default: patch template: | [![Downloads for this release](https://img.shields.io/github/downloads/JohNan/pyplaato/v$RESOLVED_VERSION/total.svg)](https://github.com/JohNan/pyplaato/releases/v$RESOLVED_VERSION) ## Changes $CHANGESpyplaato-0.0.19/.github/workflows/000077500000000000000000000000001471523222500170325ustar00rootroot00000000000000pyplaato-0.0.19/.github/workflows/codeql-analysis.yml000066400000000000000000000050311471523222500226440ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. name: "CodeQL" on: push: branches: [master] pull_request: # The branches below must be a subset of the branches above branches: [master] schedule: - cron: '0 5 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest strategy: fail-fast: false matrix: # Override automatic language detection by changing the below list # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['python'] # Learn more... # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 # If this run was triggered by a pull request event, then checkout # the head of the pull request instead of the merge commit. - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 pyplaato-0.0.19/.github/workflows/pythonpublish.yml000066400000000000000000000015071471523222500224700ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [published] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.pypi }} run: | python setup.py sdist bdist_wheel twine upload dist/* pyplaato-0.0.19/.github/workflows/release-drafter.yaml000066400000000000000000000004651471523222500227700ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master jobs: update_release_draft: name: Update release draft runs-on: ubuntu-latest steps: - name: Create Release uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} pyplaato-0.0.19/.github/workflows/test.yml000066400000000000000000000010161471523222500205320ustar00rootroot00000000000000name: Unit Testing on: pull_request: branches: - master jobs: unti_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements_test.txt -r requirements.txt pip install -e . - name: Run unit tests run: python -m pytest --import-mode=append tests/ pyplaato-0.0.19/.gitignore000066400000000000000000000035651471523222500154360ustar00rootroot00000000000000# Hide some OS X stuff .DS_Store .AppleDouble .LSOverride Icon # Thumbnails ._* # IntelliJ IDEA .idea *.iml # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/pyplaato-0.0.19/LICENSE000066400000000000000000000020561471523222500144450ustar00rootroot00000000000000MIT License Copyright (c) 2020 Johan Nenzén 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. pyplaato-0.0.19/README.md000066400000000000000000000032321471523222500147140ustar00rootroot00000000000000# Python API client for fetching Plaato data Fetches data for the Plaato Keg and Plaato Airlock using the official API handed by [blynk.cc](blynk.cc) To be able to query the API an `auth_token` is required and which can be obtained by following [these](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instructions For more information about the available pins that can be retrieved please see the official [docs](https://plaato.zendesk.com/hc/en-us/articles/360003234877-Pins) from Plaato ## Usage ``` usage: cli.py [-h] -t AUTH_TOKEN -d {keg,airlock,both} [-u URL] [-k API_KEY] optional arguments: -h, --help show this help message and exit required arguments: -t AUTH_TOKEN Auth token received from Plaato -d {keg,airlock} optional arguments: -u URL Mock url -k API_KEY Header key for mock url ``` ## Available pins ### Keg ```python BEER_NAME = "v64" PERCENT_BEER_LEFT = "v48" POURING = "v49" BEER_LEFT = "v51" BEER_LEFT_UNIT = "v74" TEMPERATURE = "v56" UNIT_TYPE = "v71" MEASURE_UNIT = "v75" MASS_UNIT = "v73" VOLUME_UNIT = "v82" LAST_POUR = "v59" DATE = "v67" OG = "v65" FG = "v66" ABV = "v68" FIRMWARE_VERSION = "v93" LEAK_DETECTION = "v83" MODE = "v88" ``` ### AirLock ```python BPM = "v102" TEMPERATURE = "v103" BATCH_VOLUME = "v104" OG = "v105" SG = "v106" ABV = "v107" TEMPERATURE_UNIT = "v108" VOLUME_UNIT = "v109" BUBBLES = "v110" CO2_VOLUME = "v119" ``` ### Disclaimer This python library was not made by Plaato. It is not official, not developed, and not supported by Plaato. pyplaato-0.0.19/cli.py000066400000000000000000000045161471523222500145640ustar00rootroot00000000000000import argparse import sys import aiohttp import asyncio from datetime import datetime from pyplaato.plaato import ( Plaato, PlaatoDeviceType ) async def go(args): headers = {} if args.api_key: headers["x-api-key"] = args.api_key plaato = Plaato(args.auth_token, args.url, headers) async with aiohttp.ClientSession() as session: if args.device == 'keg': device_type = PlaatoDeviceType.Keg if args.device == 'airlock': device_type = PlaatoDeviceType.Airlock result = await plaato.get_data(session, device_type) print(f"Device type: {result.device_type}") print(f"Name: {result.name}") print(f"Firmware: {result.firmware_version}") print(f"Date: {datetime.fromtimestamp(result.date).strftime('%x')}") print("Sensors:") for key, attr in result.sensors.items(): print(f"\t{result.get_sensor_name(key)}: {attr} {result.get_unit_of_measurement(key)}") print("Binary Sensors:") for key, attr in result.binary_sensors.items(): print(f"\t{result.get_sensor_name(key)}: {attr}") print("Attributes:") for key, attr in result.attributes.items(): print(f"\t{key}: {attr}") def main(): parser = argparse.ArgumentParser() required_argument = parser.add_argument_group('required arguments') optional_argument = parser.add_argument_group('optional arguments') required_argument.add_argument('-t', dest='auth_token', help='Auth token received from Plaato', required=True) required_argument.add_argument('-d', action='store', dest='device', choices=['keg', 'airlock'], required=True) optional_argument.add_argument('-u', dest='url', help='Mock url') optional_argument.add_argument('-k', dest='api_key', help='Header key for mock url') args = parser.parse_args() loop = asyncio.get_event_loop() loop.run_until_complete(go(args)) loop.close() if __name__ == '__main__': try: main() except KeyboardInterrupt: print('Aborting..') sys.exit(1) pyplaato-0.0.19/pyplaato/000077500000000000000000000000001471523222500152665ustar00rootroot00000000000000pyplaato-0.0.19/pyplaato/__init__.py000066400000000000000000000000001471523222500173650ustar00rootroot00000000000000pyplaato-0.0.19/pyplaato/const.py000066400000000000000000000010231471523222500167620ustar00rootroot00000000000000URL = "http://plaato.blynk.cc/{auth_token}/get" # Units UNIT_TEMP_CELSIUS = "°C" UNIT_TEMP_FAHRENHEIT = "°F" UNIT_PERCENTAGE = "%" UNIT_OZ = "oz" UNIT_LITRE = "l" UNIT_BUBBLES_PER_MINUTE = "bpm" METRIC = "1" # Webhook attributes ATTR_DEVICE_ID = "device_id" ATTR_DEVICE_NAME = "device_name" ATTR_TEMP_UNIT = "temp_unit" ATTR_VOLUME_UNIT = "volume_unit" ATTR_BPM = "bpm" ATTR_TEMP = "temp" ATTR_SG = "sg" ATTR_OG = "og" ATTR_BUBBLES = "bubbles" ATTR_ABV = "abv" ATTR_CO2_VOLUME = "co2_volume" ATTR_BATCH_VOLUME = "batch_volume"pyplaato-0.0.19/pyplaato/models/000077500000000000000000000000001471523222500165515ustar00rootroot00000000000000pyplaato-0.0.19/pyplaato/models/__init__.py000066400000000000000000000000001471523222500206500ustar00rootroot00000000000000pyplaato-0.0.19/pyplaato/models/airlock.py000066400000000000000000000102301471523222500205430ustar00rootroot00000000000000from enum import Enum from ..const import * from .device import PlaatoDevice, PlaatoDeviceType from .pins import PinsBase class PlaatoAirlock(PlaatoDevice): """Class for holding a Plaato Airlock""" device_type = PlaatoDeviceType.Airlock def __init__(self, attrs): self.bmp = attrs.get(self.Pins.BPM, None) self.temperature_unit = attrs.get(self.Pins.TEMPERATURE_UNIT, None) self.volume_unit = attrs.get(self.Pins.VOLUME_UNIT, None) self.bubbles = attrs.get(self.Pins.BUBBLES, None) self.batch_volume = attrs.get(self.Pins.BATCH_VOLUME, None) self.__sg = attrs.get(self.Pins.SG, None) self.og = attrs.get(self.Pins.OG, None) self.__abv = attrs.get(self.Pins.ABV, None) self.__co2_volume = attrs.get(self.Pins.CO2_VOLUME, None) self.__temperature = attrs.get(self.Pins.TEMPERATURE, None) def __repr__(self): return (f"{self.__class__.__name__} -> " f"BMP: {self.bmp}, " f"Temp: {self.temperature}") @classmethod def from_web_hook(cls, data): attrs = { PlaatoAirlock.Pins.BPM: data.get(ATTR_BPM, None), PlaatoAirlock.Pins.TEMPERATURE_UNIT: data.get(ATTR_TEMP_UNIT,None), PlaatoAirlock.Pins.VOLUME_UNIT: data.get(ATTR_VOLUME_UNIT,None), PlaatoAirlock.Pins.BUBBLES: data.get(ATTR_BUBBLES, None), PlaatoAirlock.Pins.BATCH_VOLUME: data.get(ATTR_BATCH_VOLUME,None), PlaatoAirlock.Pins.SG: data.get(ATTR_SG, None), PlaatoAirlock.Pins.OG: data.get(ATTR_OG, None), PlaatoAirlock.Pins.ABV: data.get(ATTR_ABV, None), PlaatoAirlock.Pins.CO2_VOLUME: data.get(ATTR_CO2_VOLUME,None), PlaatoAirlock.Pins.TEMPERATURE: data.get(ATTR_TEMP, None) } return PlaatoAirlock(attrs) @property def temperature(self): if self.__temperature is not None: try: return round(float(self.__temperature), 1) except ValueError: return self.__temperature @property def abv(self): if self.__abv is not None: return round(float(self.__abv), 2) @property def sg(self): if self.__sg is not None: return round(float(self.__sg), 3) @property def co2_volume(self): if self.__co2_volume is not None: return round(float(self.__co2_volume), 2) @property def name(self) -> str: return "Airlock" def get_sensor_name(self, pin: PinsBase) -> str: names = { self.Pins.BPM: "Bubbles per Minute", self.Pins.TEMPERATURE: "Temperature", self.Pins.BATCH_VOLUME: "Batch Volume", self.Pins.OG: "Original Gravity", self.Pins.SG: "Specific Gravity", self.Pins.ABV: "Alcohol by Volume", self.Pins.BUBBLES: "Bubbles", self.Pins.CO2_VOLUME: "CO2 Volume", } return names.get(pin, pin.name) @property def sensors(self) -> dict: return { self.Pins.BPM: self.bmp, self.Pins.TEMPERATURE: self.temperature, self.Pins.BATCH_VOLUME: self.batch_volume, self.Pins.OG: self.og, self.Pins.SG: self.sg, self.Pins.ABV: self.abv, self.Pins.BUBBLES: self.bubbles, self.Pins.CO2_VOLUME: self.co2_volume, } def get_unit_of_measurement(self, pin: PinsBase): if pin == self.Pins.TEMPERATURE: return self.temperature_unit if pin == self.Pins.BATCH_VOLUME or pin == self.Pins.CO2_VOLUME: return self.volume_unit if pin == self.Pins.BPM: return UNIT_BUBBLES_PER_MINUTE if pin == self.Pins.ABV: return UNIT_PERCENTAGE return "" # noinspection PyTypeChecker @staticmethod def pins(): return list(PlaatoAirlock.Pins) class Pins(PinsBase, Enum): BPM = "v102" TEMPERATURE = "v103" BATCH_VOLUME = "v104" OG = "v105" SG = "v106" ABV = "v107" TEMPERATURE_UNIT = "v108" VOLUME_UNIT = "v109" BUBBLES = "v110" CO2_VOLUME = "v119" pyplaato-0.0.19/pyplaato/models/device.py000066400000000000000000000024261471523222500203660ustar00rootroot00000000000000from abc import abstractmethod, ABC from datetime import datetime from enum import Enum from .pins import PinsBase class PlaatoDeviceType(str, Enum): Airlock = "Airlock" Keg = "Keg" class PlaatoDevice(ABC): @property @abstractmethod def device_type(self) -> PlaatoDeviceType: pass @property @abstractmethod def name(self) -> str: pass @property def date(self) -> float: return datetime.now().timestamp() @property def firmware_version(self) -> str: return "" @property @abstractmethod def sensors(self) -> dict: """Convenience method for Home Assistant""" pass @property def binary_sensors(self) -> dict: """Convenience method for Home Assistant""" return {} @property def attributes(self) -> dict: """Convenience method for Home Assistant""" return {} @abstractmethod def get_sensor_name(self, pin: PinsBase) -> str: """Convenience method for Home Assistant""" pass @abstractmethod def get_unit_of_measurement(self, pin: PinsBase): """Convenience method to get unit of measurement for Home Assistant""" pass @staticmethod @abstractmethod def pins() -> list: pass pyplaato-0.0.19/pyplaato/models/keg.py000066400000000000000000000143321471523222500176740ustar00rootroot00000000000000from datetime import datetime from enum import Enum import dateutil.parser from .device import PlaatoDevice, PlaatoDeviceType from .pins import PinsBase from ..const import UNIT_TEMP_CELSIUS, UNIT_TEMP_FAHRENHEIT, UNIT_PERCENTAGE, \ METRIC, UNIT_OZ, UNIT_LITRE class PlaatoKeg(PlaatoDevice): """Class for holding a Plaato Keg""" device_type = PlaatoDeviceType.Keg def __init__(self, attrs): self.beer_left_unit = attrs.get(self.Pins.BEER_LEFT_UNIT, None) self.volume_unit = attrs.get(self.Pins.VOLUME_UNIT, None) self.mass_unit = attrs.get(self.Pins.MASS_UNIT, None) self.measure_unit = attrs.get(self.Pins.MEASURE_UNIT, None) self.og = attrs.get(self.Pins.OG, None) self.fg = attrs.get(self.Pins.FG, None) self.__mode = attrs.get(self.Pins.MODE, None) self.__firmware_version = attrs.get(self.Pins.FIRMWARE_VERSION, None) self.__leak_detection = attrs.get(self.Pins.LEAK_DETECTION, None) self.__abv = attrs.get(self.Pins.ABV, None) self.__name = attrs.get(self.Pins.BEER_NAME, "Beer") self.__percent_beer_left = attrs.get(self.Pins.PERCENT_BEER_LEFT, None) self.__pouring = attrs.get(self.Pins.POURING, False) self.__beer_left = attrs.get(self.Pins.BEER_LEFT, None) self.__temperature = attrs.get(self.Pins.TEMPERATURE, None) self.__unit_type = attrs.get(self.Pins.UNIT_TYPE, None) self.__last_pour = attrs.get(self.Pins.LAST_POUR, None) self.__date = attrs.get(self.Pins.DATE, None) def __repr__(self): return f"{self.__class__.__name__} -> " \ f"Beer Left: {self.beer_left}, " \ f"Temp: {self.temperature}, " \ f"Pouring: {self.pouring}" @property def date(self) -> float: if self.__date is not None and self.__date and not self.__date.isspace(): date = dateutil.parser.parse(self.__date) return date.timestamp() return super().date @property def temperature(self): if self.__temperature is not None: return round(float(self.__temperature), 1) @property def temperature_unit(self): if self.__unit_type == METRIC: return UNIT_TEMP_CELSIUS return UNIT_TEMP_FAHRENHEIT @property def beer_left(self): if self.__beer_left is not None: return round(float(self.__beer_left), 2) @property def percent_beer_left(self): if self.__percent_beer_left is not None: return round(self.__percent_beer_left, 2) @property def last_pour(self): if self.__last_pour is not None: return round(float(self.__last_pour), 2) @property def last_pour_unit(self): if self.__unit_type == METRIC: return UNIT_LITRE return UNIT_OZ @property def abv(self): if self.__abv is not None and not self.__abv: return round(float(self.__abv), 2) @property def pouring(self): """ 0 = Not Pouring 255 = Pouring :return: True if 255 = Pouring else False """ return self.__pouring == "255" @property def leak_detection(self): """ 1 = Leaking 0 = Not Leaking :return: True if 1 = Leaking else False """ return self.__leak_detection == "1" @property def mode(self): """ 1 = Beer 2 = Co2 """ return "Beer" if self.__mode == "1" else "Co2" @property def name(self) -> str: return self.__name @property def firmware_version(self) -> str: return self.__firmware_version def get_sensor_name(self, pin: PinsBase) -> str: names = { self.Pins.PERCENT_BEER_LEFT: "Percent Beer Left", self.Pins.POURING: "Pouring", self.Pins.BEER_LEFT: "Beer Left", self.Pins.TEMPERATURE: "Temperature", self.Pins.LAST_POUR: "Last Pour Amount", self.Pins.OG: "Original Gravity", self.Pins.FG: "Final Gravity", self.Pins.ABV: "Alcohol by Volume", self.Pins.LEAK_DETECTION: "Leaking", self.Pins.MODE: "Mode", self.Pins.DATE: "Keg Date", self.Pins.BEER_NAME: "Beer Name" } return names.get(pin, pin.name) @property def sensors(self) -> dict: return { self.Pins.PERCENT_BEER_LEFT: self.percent_beer_left, self.Pins.BEER_LEFT: self.beer_left, self.Pins.TEMPERATURE: self.temperature, self.Pins.LAST_POUR: self.last_pour } @property def binary_sensors(self) -> dict: return { self.Pins.LEAK_DETECTION: self.leak_detection, self.Pins.POURING: self.pouring, } @property def attributes(self) -> dict: return { self.get_sensor_name(self.Pins.BEER_NAME): self.name, self.get_sensor_name(self.Pins.DATE): datetime.fromtimestamp(self.date).strftime('%x'), self.get_sensor_name(self.Pins.MODE): self.mode, self.get_sensor_name(self.Pins.OG): self.og, self.get_sensor_name(self.Pins.FG): self.fg, self.get_sensor_name(self.Pins.ABV): self.abv } def get_unit_of_measurement(self, pin: PinsBase): if pin == self.Pins.BEER_LEFT: return self.beer_left_unit if pin == self.Pins.TEMPERATURE: return self.temperature_unit if pin == self.Pins.LAST_POUR: return self.last_pour_unit if pin == self.Pins.ABV or pin == self.Pins.PERCENT_BEER_LEFT: return UNIT_PERCENTAGE return "" # noinspection PyTypeChecker @staticmethod def pins(): return list(PlaatoKeg.Pins) class Pins(PinsBase, Enum): BEER_NAME = "v64" PERCENT_BEER_LEFT = "v48" POURING = "v49" BEER_LEFT = "v51" BEER_LEFT_UNIT = "v74" TEMPERATURE = "v56" UNIT_TYPE = "v71" MEASURE_UNIT = "v75" MASS_UNIT = "v73" VOLUME_UNIT = "v82" LAST_POUR = "v59" DATE = "v67" OG = "v65" FG = "v66" ABV = "v68" FIRMWARE_VERSION = "v93" LEAK_DETECTION = "v83" MODE = "v88" pyplaato-0.0.19/pyplaato/models/pins.py000066400000000000000000000001101471523222500200640ustar00rootroot00000000000000from enum import Enum class PinsBase(str, Enum): """Base class""" pyplaato-0.0.19/pyplaato/plaato.py000066400000000000000000000062721471523222500171270ustar00rootroot00000000000000"""Fetch data from Plaato Airlock and Keg""" from json import JSONDecodeError from typing import Optional from aiohttp import ClientSession import logging from .models.airlock import PlaatoAirlock from .models.device import PlaatoDevice, PlaatoDeviceType from .models.keg import PlaatoKeg from .models.pins import PinsBase from .const import * class Plaato(object): """Represents a Plaato device""" def __init__(self, auth_token="NO_AUTH_TOKEN", url=URL, headers=None): if headers is None: headers = {} self.__headers = headers if not url: url = URL self.__url = url.replace('{auth_token}', auth_token) async def get_data( self, session: ClientSession, device_type: PlaatoDeviceType ) -> PlaatoDevice: if device_type == PlaatoDeviceType.Keg: return await self.get_keg_data(session) if device_type == PlaatoDeviceType.Airlock: return await self.get_airlock_data(session) pass async def get_keg_data(self, session: ClientSession) -> PlaatoKeg: """Fetch values for each pin""" result = {} for pin in PlaatoKeg.pins(): result[pin] = await self.fetch_data(session, pin) errors = Plaato._get_errors_as_string(result) if errors: logging.getLogger(__name__) \ .warning(f"Failed to get value for {errors}") return PlaatoKeg(result) async def get_airlock_data(self, session: ClientSession) -> PlaatoAirlock: """Fetch values for each pin""" result = {} for pin in PlaatoAirlock.pins(): result[pin] = await self.fetch_data(session, pin) errors = Plaato._get_errors_as_string(result) if errors: logging.getLogger(__name__) \ .warning(f"Failed to get value for {errors}") return PlaatoAirlock(result) async def fetch_data(self, session: ClientSession, pin: PinsBase): """Fetches the data for a specific pin""" async with session.get( url=f"{self.__url}/{pin.value}", headers=self.__headers ) as resp: result = None try: data = await resp.json(content_type=None) if Plaato._iterable(data): if "error" in data: logging.getLogger(__name__) \ .debug(f"Pin {pin.name} not found") elif len(data) == 1: result = data[0] else: result = data except JSONDecodeError as e: logging.getLogger(__name__)\ .warning(f"Failed to decode json for pin {pin} - {e.msg}") return result @staticmethod def _get_errors_as_string(result: dict) -> Optional[str]: errors = dict(filter(lambda elem: elem[1] is None, result.items())) if errors: return ', '.join(map(lambda elem: elem.name, errors.keys())) return None @staticmethod def _iterable(obj): try: iter(obj) except TypeError: return False else: return True pyplaato-0.0.19/requirements.txt000066400000000000000000000000451471523222500167200ustar00rootroot00000000000000aiohttp~=3.9.5 python-dateutil==2.8.1pyplaato-0.0.19/requirements_test.txt000066400000000000000000000000151471523222500177540ustar00rootroot00000000000000pytest==7.0.1pyplaato-0.0.19/setup.py000066400000000000000000000015261471523222500151530ustar00rootroot00000000000000import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="pyplaato", version="0.0.19", author="JohNan", author_email="johan.nanzen@gmail.com", description="Asynchronous Python client for getting Plaato Airlock and Keg data", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/JohNan/pyplaato", packages=setuptools.find_packages(exclude=["tests", "tests.*"]), classifiers=[ "Framework :: AsyncIO", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", ], python_requires='>=3', ) pyplaato-0.0.19/tests/000077500000000000000000000000001471523222500145775ustar00rootroot00000000000000pyplaato-0.0.19/tests/__init__.py000066400000000000000000000000001471523222500166760ustar00rootroot00000000000000pyplaato-0.0.19/tests/models/000077500000000000000000000000001471523222500160625ustar00rootroot00000000000000pyplaato-0.0.19/tests/models/__init__.py000066400000000000000000000000001471523222500201610ustar00rootroot00000000000000pyplaato-0.0.19/tests/models/test_keg.py000066400000000000000000000022111471523222500202350ustar00rootroot00000000000000import os, time from datetime import datetime from unittest import mock from pyplaato.models.keg import PlaatoKeg def setup_module(module): os.environ['TZ'] = 'Europe/London' time.tzset() def test_date_prop_with_value_returns_its_timestamp(): pins = PlaatoKeg.Pins keg = PlaatoKeg({pins.DATE: "10/1/2022"}) assert 1664578800.0 == keg.date @mock.patch("pyplaato.models.device.datetime") def test_date_prop_with_no_value_returns_current_timestamp(m_datetime): now = datetime(2022, 10, 1) m_datetime.now.return_value = now keg = PlaatoKeg({}) assert now.timestamp() == keg.date @mock.patch("pyplaato.models.device.datetime") def test_date_prop_with_empty_string(m_datetime): now = datetime(2022, 10, 1) m_datetime.now.return_value = now pins = PlaatoKeg.Pins keg = PlaatoKeg({pins.DATE: ""}) assert now.timestamp() == keg.date @mock.patch("pyplaato.models.device.datetime") def test_date_prop_with_space_string(m_datetime): now = datetime(2022, 10, 1) m_datetime.now.return_value = now pins = PlaatoKeg.Pins keg = PlaatoKeg({pins.DATE: " "}) assert now.timestamp() == keg.date