pax_global_header00006660000000000000000000000064145471660440014525gustar00rootroot0000000000000052 comment=9017462248d300e94a7827ae077de1c69dbe3901 Danielhiversen-pyAirthings-9017462/000077500000000000000000000000001454716604400171625ustar00rootroot00000000000000Danielhiversen-pyAirthings-9017462/.github/000077500000000000000000000000001454716604400205225ustar00rootroot00000000000000Danielhiversen-pyAirthings-9017462/.github/workflows/000077500000000000000000000000001454716604400225575ustar00rootroot00000000000000Danielhiversen-pyAirthings-9017462/.github/workflows/code_checker.yml000066400000000000000000000020301454716604400256730ustar00rootroot00000000000000name: Code checker on: push: pull_request: jobs: validate: runs-on: "ubuntu-latest" strategy: matrix: python-version: - "3.7" - "3.8" - "3.9" - "3.10-dev" steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v2 with: path: ~/.cache/pip key: ${{ hashFiles('setup.py') }} - name: Install depencency run: | pip install aiohttp async_timeout pip install dlint flake8 flake8-bandit flake8-bugbear flake8-deprecated flake8-executable isort pylint - name: Flake8 Code Linter run: | flake8 airthings/ --max-line-length=120 - name: isort run: | isort **/*.py - name: Pylint Code Linter run: | pylint --disable=C,R --enable=unidiomatic-typecheck airthings Danielhiversen-pyAirthings-9017462/.github/workflows/python-publish.yml000066400000000000000000000015151454716604400262710ustar00rootroot00000000000000# 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: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 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_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* Danielhiversen-pyAirthings-9017462/.gitignore000066400000000000000000000034071454716604400211560ustar00rootroot00000000000000# 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/ Danielhiversen-pyAirthings-9017462/LICENSE000066400000000000000000000020671454716604400201740ustar00rootroot00000000000000MIT License Copyright (c) 2021 Daniel Hjelseth Høyer 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. Danielhiversen-pyAirthings-9017462/README.md000066400000000000000000000003121454716604400204350ustar00rootroot00000000000000# pyAirthings Python Airthings Example usage in Home Assistant: https://github.com/home-assistant/core/blob/6e7bc65e2e31b82becc5d5ac474712af5c019e4d/homeassistant/components/airthings/__init__.py#L35 Danielhiversen-pyAirthings-9017462/airthings/000077500000000000000000000000001454716604400211525ustar00rootroot00000000000000Danielhiversen-pyAirthings-9017462/airthings/__init__.py000066400000000000000000000153461454716604400232740ustar00rootroot00000000000000"""Support for Airthings sensor.""" from __future__ import annotations import asyncio from dataclasses import dataclass import json import logging from aiohttp import ClientError import async_timeout _LOGGER = logging.getLogger(__name__) API_URL = "https://ext-api.airthings.com/v1/" TIMEOUT = 10 @dataclass class AirthingsLocation: """Airthings location.""" location_id: str name: str @classmethod def init_from_response(cls, response): """Class method.""" return cls( response.get("id"), response.get("name"), ) @dataclass class AirthingsDevice: """Airthings device.""" device_id: str name: str sensors: dict[str, float | None] is_active: bool location_name: str device_type: str | None product_name: str | None @classmethod def init_from_response(cls, response, location_name, device): """Class method.""" return cls( response.get("id"), response.get("segment").get("name"), response.get("data"), response.get("segment").get("isActive"), location_name, device.get('deviceType'), device.get('productName'), ) @property def sensor_types(self): """Sensor types.""" return self.sensors.keys() class AirthingsError(Exception): """General Airthings exception occurred.""" class AirthingsConnectionError(AirthingsError): """ConnectionError Airthings occurred.""" class AirthingsAuthError(AirthingsError): """AirthingsAuthError Airthings occurred.""" class Airthings: """Airthings data handler.""" def __init__(self, client_id, secret, websession): """Init Airthings data handler.""" self._client_id = client_id self._secret = secret self._websession = websession self._access_token = None self._locations = [] self._devices = {} async def update_devices(self): """Update data.""" if not self._locations: response = await self._request(API_URL + "locations") json_data = await response.json() self._locations = [] for location in json_data.get("locations"): self._locations.append(AirthingsLocation.init_from_response(location)) if not self._devices: response = await self._request(API_URL + "devices") json_data = await response.json() self._devices = {} for device in json_data.get("devices"): self._devices[device['id']] = device res = {} for location in self._locations: if not location.location_id: continue response = await self._request( API_URL + f"/locations/{location.location_id}/latest-samples" ) if response is None: continue json_data = await response.json() if json_data is None: continue if devices := json_data.get("devices"): for device in devices: id = device.get('id') res[id] = AirthingsDevice.init_from_response( device, location.name, self._devices.get(id) ) else: _LOGGER.debug("No devices in location '%s'", location.name) return res async def _request(self, url, json_data=None, retry=3): _LOGGER.debug("Request %s %s, %s", url, retry, json_data) if self._access_token is None: self._access_token = await get_token( self._websession, self._client_id, self._secret ) if self._access_token is None: return None headers = {"Authorization": self._access_token} try: async with async_timeout.timeout(TIMEOUT): if json_data: response = await self._websession.post( url, json=json_data, headers=headers ) else: response = await self._websession.get(url, headers=headers) if response.status != 200: if retry > 0 and response.status != 429: self._access_token = None return await self._request(url, json_data, retry=retry - 1) _LOGGER.error( "Error connecting to Airthings, response: %s %s", response.status, response.reason, ) raise AirthingsError( f"Error connecting to Airthings, response: {response.reason}" ) except ClientError as err: self._access_token = None _LOGGER.error("Error connecting to Airthings: %s ", err, exc_info=True) raise AirthingsError from err except asyncio.TimeoutError as err: self._access_token = None if retry > 0: return await self._request(url, json_data, retry=retry - 1) _LOGGER.error("Timed out when connecting to Airthings") raise AirthingsError from err return response async def get_token(websession, client_id, secret, retry=3, timeout=10): """Get token for Airthings.""" try: async with async_timeout.timeout(timeout): response = await websession.post( "https://accounts-api.airthings.com/v1/token", headers={ "Content-type": "application/x-www-form-urlencoded", "Accept": "application/json", }, data={ "grant_type": "client_credentials", "client_id": client_id, "client_secret": secret, }, ) except ClientError as err: if retry > 0: return await get_token(websession, client_id, secret, retry - 1, timeout) _LOGGER.error("Error getting token Airthings: %s ", err, exc_info=True) raise AirthingsConnectionError from err except asyncio.TimeoutError as err: if retry > 0: return await get_token(websession, client_id, secret, retry - 1, timeout) _LOGGER.error("Timed out when connecting to Airthings for token") raise AirthingsConnectionError from err if response.status != 200: _LOGGER.error( "Airthings: Failed to login to retrieve token: %s %s", response.status, response.reason, ) raise AirthingsAuthError(f"Failed to login to retrieve token {response.reason}") token_data = json.loads(await response.text()) return token_data.get("access_token") Danielhiversen-pyAirthings-9017462/setup.py000066400000000000000000000013111454716604400206700ustar00rootroot00000000000000from setuptools import setup setup( name="airthings_cloud", packages=["airthings"], install_requires=["aiohttp>=3.0.6", "async_timeout>=3.0.0"], version="0.2.0", description="A python3 library to communicate with Airthings devices", python_requires=">=3.7.0", author="Daniel Hjelseth Høyer", author_email="github@dahoiv.net", url="https://github.com/Danielhiversen/pyAirthings", license="MIT", classifiers=[ "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], )