pax_global_header00006660000000000000000000000064147576431010014522gustar00rootroot0000000000000052 comment=81221f069ec6be973f6dc89a7325f8e2f626c3f7 stookwijzer-1.6.1/000077500000000000000000000000001475764310100141215ustar00rootroot00000000000000stookwijzer-1.6.1/.github/000077500000000000000000000000001475764310100154615ustar00rootroot00000000000000stookwijzer-1.6.1/.github/workflows/000077500000000000000000000000001475764310100175165ustar00rootroot00000000000000stookwijzer-1.6.1/.github/workflows/python-publish.yml000066400000000000000000000015131475764310100232260ustar00rootroot00000000000000# This workflow 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@v4 - 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@v1.8.11 with: password: ${{ secrets.PYPI_API_TOKEN }} stookwijzer-1.6.1/.gitignore000066400000000000000000000034071475764310100161150ustar00rootroot00000000000000# 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/ stookwijzer-1.6.1/LICENSE000066400000000000000000000020541475764310100151270ustar00rootroot00000000000000MIT License Copyright (c) 2022 fwestenberg 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. stookwijzer-1.6.1/README.md000066400000000000000000000004131475764310100153760ustar00rootroot00000000000000# Stookwijzer This is a Python package for the Stookwijzer API. The Stookwijzer data is retrieved from the [Atlas Leefomgeving](https://www.atlasleefomgeving.nl/stookwijzer) API. ## Installation ## Stookwijzer requires Python 3.6+ to run. `pip install stookwijzer` stookwijzer-1.6.1/setup.cfg000066400000000000000000000000501475764310100157350ustar00rootroot00000000000000[metadata] description-file = README.md stookwijzer-1.6.1/setup.py000066400000000000000000000015221475764310100156330ustar00rootroot00000000000000from distutils.core import setup setup( name = 'stookwijzer', packages = ['stookwijzer'], version = '1.6.1', license='MIT', description = 'Stookwijzer package', long_description_content_type="text/markdown", long_description='Stookwijzer package', author = 'fwestenberg', author_email = '', url = 'https://github.com/fwestenberg/stookwijzer', download_url = 'https://github.com/fwestenberg/stookwijzer/releases/latest', keywords = ['Stookwijzer', 'Home-Assistant'], install_requires=[ 'aiohttp', 'pytz' ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13' ], ) stookwijzer-1.6.1/stookwijzer/000077500000000000000000000000001475764310100165135ustar00rootroot00000000000000stookwijzer-1.6.1/stookwijzer/__init__.py000066400000000000000000000000641475764310100206240ustar00rootroot00000000000000from stookwijzer.stookwijzerapi import Stookwijzer stookwijzer-1.6.1/stookwijzer/const.py000066400000000000000000000001501475764310100202070ustar00rootroot00000000000000"""Constants for the Stookwijzer integration.""" import logging LOGGER = logging.getLogger(__package__)stookwijzer-1.6.1/stookwijzer/example.py000066400000000000000000000015371475764310100205260ustar00rootroot00000000000000"""Example usage of the Stookwijzer API.""" import aiohttp import asyncio import stookwijzerapi async def main(): xy = await stookwijzerapi.Stookwijzer.async_transform_coordinates(52.123456, 6.123456) print(f"x: {xy['x']}") print(f"y: {xy['y']}") if xy: session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) sw = stookwijzerapi.Stookwijzer(session, xy['x'], xy['y']) await sw.async_update() print() print(f"advice: {sw.advice}") print(f"windspeed bft: {sw.windspeed_bft}") print(f"windspeed ms: {sw.windspeed_ms}") print(f"lki: {sw.lki}") print() print(f"forecast_advice: {await sw.async_get_forecast()}") await session.close() if __name__ == "__main__": asyncio.run(main()) stookwijzer-1.6.1/stookwijzer/stookwijzerapi.py000066400000000000000000000130601475764310100221510ustar00rootroot00000000000000"""The Stookwijze API.""" from datetime import datetime, timedelta import aiohttp import asyncio import json import logging import pytz _LOGGER = logging.getLogger(__name__) class Stookwijzer: """The Stookwijze API.""" def __init__(self, session: aiohttp.ClientSession, x: float, y: float): self._boundary_box = self.get_boundary_box(x, y) self._advice = None self._alert = None self._last_updated = None self._stookwijzer = None self._session = session @property def advice(self) -> str | None: """Return the advice.""" return self._advice @property def windspeed_bft(self) -> int | None: """Return the windspeed in bft.""" return self.get_property("wind_bft") @property def windspeed_ms(self) -> float | None: """Return the windspeed in m/s.""" windspeed = self.get_property("wind") return round(float(windspeed), 1) if windspeed else windspeed @property def lki(self) -> int | None: """Return the lki.""" return self.get_property("lki") @property def last_updated(self) -> datetime | None: """Get the last updated date.""" return self._last_updated @staticmethod async def async_transform_coordinates(latitude: float, longitude: float) -> dict | None: """Transform the coordinates from EPSG:4326 to EPSG:28992.""" x0 = 155000 y0 = 463000 f0 = 52.15517440 # f => phi l0 = 5.38720621 # l => lambda Rp = [0, 1, 2, 0, 1, 3, 1, 0, 2] Rq = [1, 1, 1, 3, 0, 1, 3, 2, 3] Rpq = [ 190094.945, -11832.228, -114.221, -32.391, -0.705, -2.34, -0.608, -0.008, 0.148, ] Sp = [1, 0, 2, 1, 3, 0, 2, 1, 0, 1] Sq = [0, 2, 0, 2, 0, 1, 2, 1, 4, 4] Spq = [ 309056.544, 3638.893, 73.077, -157.984, 59.788, 0.433, -6.439, -0.032, 0.092, -0.054, ] df = 0.36 * (latitude - f0) dl = 0.36 * (longitude - l0) x = x0 y = y0 x += sum(Rpq[i] * (df ** Rp[i]) * (dl ** Rq[i]) for i in range(9)) y += sum(Spq[i] * (df ** Sp[i]) * (dl ** Sq[i]) for i in range(10)) return {"x": x, "y": y} async def async_update(self) -> None: """Get the stookwijzer data.""" self._stookwijzer = await self.async_get_stookwijzer() advice = self.get_property("advies_0") if advice: self._advice = self.get_color(advice) self._last_updated = datetime.now() async def async_get_forecast(self) -> list[dict[str, str]]: """Return the forecast array.""" forecast = [] runtime = self.get_property("model_runtime") if not runtime: return None dt = datetime.strptime(runtime, "%d-%m-%Y %H:%M") localdt = dt.astimezone(pytz.timezone("Europe/Amsterdam")) for offset in range(0, 19, 6): forecast.append(await self.get_forecast_at_offset(localdt, offset)) return forecast async def get_forecast_at_offset( self, runtime: datetime, offset: int ) -> dict[str, str]: """Get forecast at a certain offset.""" dt = {"datetime": (runtime + timedelta(hours=offset)).isoformat()} forecast = { "advice": self.get_color(self.get_property("advies_" + str(offset))), "final": self.get_property("definitief_" + str(offset)) == "True", } dt.update(forecast) return dt def get_boundary_box(self, x: float, y: float) -> str | None: """Create a boundary box with the coordinates""" return str(x) + "%2C" + str(y) + "%2C" + str(x + 10) + "%2C" + str(y + 10) def get_color(self, advice: str) -> str: """Convert the Stookwijzer data into a color.""" if advice == "0": return "code_yellow" if advice == "1": return "code_orange" if advice == "2": return "code_red" return "" def get_property(self, prop: str) -> str: """Get a feature from the JSON data""" try: return str(self._stookwijzer["features"][0]["properties"][prop]) except (KeyError, IndexError, TypeError): _LOGGER.error("Property %s not available", prop) return "" async def async_get_stookwijzer(self): """Get the stookwijzer data.""" url = ( "https://data.rivm.nl/geo/alo/wms?service=WMS&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&FORMAT=image/png&TRANSPARENT=true&QUERY_LAYERS=stookwijzer_v2&LAYERS=stookwijzer_v2&servicekey=82b124ad-834d-4c10-8bd0-ee730d5c1cc8&STYLES=&BUFFER=1&EXCEPTIONS=INIMAGE&info_format=application/json&feature_count=1&I=139&J=222&WIDTH=256&HEIGHT=256&CRS=EPSG:28992&BBOX=" + self._boundary_box ) try: async with self._session.get( url=url, allow_redirects=False, timeout=10 ) as response: response = await response.read() return json.loads(response) except aiohttp.ClientConnectorError: _LOGGER.error("Error getting Stookwijzer data") return None except asyncio.TimeoutError: _LOGGER.error("Timeout getting Stookwijzer data") return None except KeyError: _LOGGER.error("Received invalid response from Stookwijzer") return None