├── .gitmodules ├── hacs.json ├── documentation └── example.png ├── custom_components └── neerslag │ ├── const.py │ ├── manifest.json │ ├── load_frontend.py │ ├── strings.json │ ├── translations │ ├── en.json │ ├── nl.json │ └── fr.json │ ├── __init__.py │ ├── config_flow.py │ └── sensor.py ├── .github └── workflows │ ├── validate-with-hassfest.yaml │ └── validate-hacs.yml ├── .gitignore └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Neerslag App", 3 | "render_readme": true, 4 | "country": ["NL", "BE"] 5 | } -------------------------------------------------------------------------------- /documentation/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aex351/home-assistant-neerslag-app/HEAD/documentation/example.png -------------------------------------------------------------------------------- /custom_components/neerslag/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Neerslag Sensor (Buienalarm / Buienradar) integration.""" 2 | 3 | DOMAIN = "neerslag" 4 | FRONTEND_SCRIPT_URL = "/neerslag-card.js" 5 | DATA_EXTRA_MODULE_URL = 'frontend_extra_module_url' -------------------------------------------------------------------------------- /.github/workflows/validate-with-hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: home-assistant/actions/hassfest@master -------------------------------------------------------------------------------- /.github/workflows/validate-hacs.yml: -------------------------------------------------------------------------------- 1 | name: Validate with HACS 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" 18 | -------------------------------------------------------------------------------- /custom_components/neerslag/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "neerslag", 3 | "name": "Neerslag App (Buienalarm / Buienradar)", 4 | "config_flow": true, 5 | "documentation": "https://github.com/aex351/home-assistant-neerslag-app", 6 | "requirements": [], 7 | "ssdp": [], 8 | "zeroconf": [], 9 | "homekit": {}, 10 | "dependencies": [], 11 | "issue_tracker": "https://github.com/aex351/home-assistant-neerslag-app/issues", 12 | "codeowners": [ 13 | "@aex351" 14 | ], 15 | "iot_class" : "cloud_polling", 16 | "version": "2025.12.03.0" 17 | } -------------------------------------------------------------------------------- /custom_components/neerslag/load_frontend.py: -------------------------------------------------------------------------------- 1 | from homeassistant.core import HomeAssistant 2 | from homeassistant.components.frontend import add_extra_js_url 3 | from homeassistant.components.http import StaticPathConfig 4 | from aiohttp import web 5 | import logging 6 | import os 7 | from .const import FRONTEND_SCRIPT_URL, DATA_EXTRA_MODULE_URL 8 | import time 9 | 10 | _LOGGER = logging.getLogger(__name__) 11 | 12 | 13 | async def setup_view(hass: HomeAssistant): 14 | dir_path = os.path.dirname(os.path.realpath(__file__)) 15 | path_to_file = os.path.join( 16 | dir_path, 17 | "home-assistant-neerslag-card", 18 | "neerslag-card.js", 19 | ) 20 | 21 | should_cache = False 22 | 23 | _LOGGER.debug( 24 | "Neerslag frontend: registering static path %s -> %s", 25 | FRONTEND_SCRIPT_URL, 26 | path_to_file, 27 | ) 28 | 29 | await hass.http.async_register_static_paths( 30 | [ 31 | StaticPathConfig( 32 | FRONTEND_SCRIPT_URL, 33 | path_to_file, 34 | should_cache, 35 | ) 36 | ] 37 | ) 38 | 39 | add_extra_js_url(hass, FRONTEND_SCRIPT_URL, es5=False) 40 | 41 | _LOGGER.debug( 42 | "Neerslag frontend: registered extra JS module URL %s", 43 | FRONTEND_SCRIPT_URL, 44 | ) -------------------------------------------------------------------------------- /custom_components/neerslag/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title" : "Neerslag App set-up", 6 | "description" : "It can take a couple of minutes before the sensors to become active.", 7 | "data": { 8 | "host": "[%key:common::config_flow::data::host%]", 9 | "username": "[%key:common::config_flow::data::username%]", 10 | "password": "[%key:common::config_flow::data::password%]", 11 | "buienradar": "Enable Buienradar", 12 | "buienradarLatitude": "Latitude", 13 | "buienradarLongitude": "Longitude", 14 | "buienalarm": "Enable Buienalarm", 15 | "buienalarmLatitude": "Latitude", 16 | "buienalarmLongitude": "Longitude", 17 | "NeerslagSensorUseHAforLocation" : "Use the location settings (latitude/longitude) of Home Assistant (Overrides all other location settings)" 18 | } 19 | } 20 | }, 21 | "error": { 22 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 23 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 24 | "unknown": "[%key:common::config_flow::error::unknown%]" 25 | }, 26 | "abort": { 27 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 28 | } 29 | }, 30 | "options":{ 31 | "step": { 32 | "init": { 33 | "title" : "Neerslag App options", 34 | "description" : "Changes (after clicking submit) take a couple of minutes to process.", 35 | "data": { 36 | "host": "[%key:common::config_flow::data::host%]", 37 | "username": "[%key:common::config_flow::data::username%]", 38 | "password": "[%key:common::config_flow::data::password%]", 39 | "buienradar": "Enable Buienradar", 40 | "buienradarLatitude": "Latitude", 41 | "buienradarLongitude": "Longitude", 42 | "buienalarm": "Enable Buienalarm", 43 | "buienalarmLatitude": "Latitude", 44 | "buienalarmLongitude": "Longitude", 45 | "NeerslagSensorUseHAforLocation" : "Use the location settings (latitude/longitude) of Home Assistant (Overrides all other location settings)" 46 | } 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /custom_components/neerslag/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "NeerslagSensorUseHAforLocation": "Use the location settings (latitude/longitude) of Home Assistant (Overrides all other location settings)", 15 | "buienalarm": "Enable Buienalarm", 16 | "buienalarmLatitude": "Latitude", 17 | "buienalarmLongitude": "Longitude", 18 | "buienradar": "Enable Buienradar", 19 | "buienradarLatitude": "Latitude", 20 | "buienradarLongitude": "Longitude", 21 | "host": "Host", 22 | "password": "Password", 23 | "username": "Username" 24 | }, 25 | "description": "It can take a couple of minutes before the sensors to become active.", 26 | "title": "Neerslag App set-up" 27 | } 28 | } 29 | }, 30 | "options": { 31 | "step": { 32 | "init": { 33 | "data": { 34 | "NeerslagSensorUseHAforLocation": "Use the location settings (latitude/longitude) of Home Assistant (Overrides all other location settings)", 35 | "buienalarm": "Enable Buienalarm", 36 | "buienalarmLatitude": "Latitude", 37 | "buienalarmLongitude": "Longitude", 38 | "buienradar": "Enable Buienradar", 39 | "buienradarLatitude": "Latitude", 40 | "buienradarLongitude": "Longitude", 41 | "host": "Host", 42 | "password": "Password", 43 | "username": "Username" 44 | }, 45 | "description": "Changes (after clicking submit) take a couple of minutes to process.", 46 | "title": "Neerslag App options" 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /custom_components/neerslag/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Het apparaat is al geconfigureerd" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "NeerslagSensorUseHAforLocation": "Gebruik de locatie instellingen (latitude/longitude) van Home Assistant (deze optie overschrijft de andere locatie instellingen)", 15 | "buienalarm": "Activeer Buienalarm", 16 | "buienalarmLatitude": "Latitude", 17 | "buienalarmLongitude": "Longitude", 18 | "buienradar": "Activeer Buienradar", 19 | "buienradarLatitude": "Latitude", 20 | "buienradarLongitude": "Longitude", 21 | "host": "Host", 22 | "password": "Password", 23 | "username": "Username" 24 | }, 25 | "description": "Het kan een paar minuten duren voordat de sensors geactiveerd zijn.", 26 | "title": "Neerslag App set-up" 27 | } 28 | } 29 | }, 30 | "options": { 31 | "step": { 32 | "init": { 33 | "data": { 34 | "NeerslagSensorUseHAforLocation": "Gebruik de locatie instellingen (latitude/longitude) van Home Assistant (deze optie overschrijft de andere locatie instellingen)", 35 | "buienalarm": "Activeer Buienalarm", 36 | "buienalarmLatitude": "Latitude", 37 | "buienalarmLongitude": "Longitude", 38 | "buienradar": "Activeer Buienradar", 39 | "buienradarLatitude": "Latitude", 40 | "buienradarLongitude": "Longitude", 41 | "host": "Host", 42 | "password": "Password", 43 | "username": "Username" 44 | }, 45 | "description": "Het kan een paar minuten duren voordat de veranderingen verwerkt zijn.", 46 | "title": "Neerslag App opties" 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /custom_components/neerslag/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "L'appareil est déjà configuré" 5 | }, 6 | "error": { 7 | "cannot_connect": "Connection impossible", 8 | "invalid_auth": "Authentification invalide", 9 | "unknown": "Erreur inconnue" 10 | }, 11 | "step": { 12 | "user": { 13 | "data": { 14 | "NeerslagSensorUseHAforLocation": "Utilisation des paramètres de localisation de Home Assistant (latitude/longitude ; outrepasse les autres paramètres de localisation)", 15 | "buienalarm": "Activer Buienalarm", 16 | "buienalarmLatitude": "Latitude", 17 | "buienalarmLongitude": "Longitude", 18 | "buienradar": "Activer Buienradar", 19 | "buienradarLatitude": "Latitude", 20 | "buienradarLongitude": "Longitude", 21 | "host": "Hôte", 22 | "password": "Mot de Passe", 23 | "username": "Nom d'utilisateur" 24 | }, 25 | "description": "Les senseurs peuvent prendre quelque minutes pour devenir actifs.", 26 | "title": "Configuration de l'application Neerslag" 27 | } 28 | } 29 | }, 30 | "options": { 31 | "step": { 32 | "init": { 33 | "data": { 34 | "NeerslagSensorUseHAforLocation": "Utilisation des paramètres de localisation de Home Assistant (latitude/longitude ; outrepasse les autres paramètres de localisation)", 35 | "buienalarm": "Activer Buienalarm", 36 | "buienalarmLatitude": "Latitude", 37 | "buienalarmLongitude": "Longitude", 38 | "buienradar": "Activer Buienradar", 39 | "buienradarLatitude": "Latitude", 40 | "buienradarLongitude": "Longitude", 41 | "host": "Hôte", 42 | "password": "Mot de Passe", 43 | "username": "Nom d'utilisateur" 44 | }, 45 | "description": "Les changements seront appliqués quelques minutes après soumission.", 46 | "title": "Options de l'application Neerslag" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | .vscode 3 | 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ 143 | 144 | # Additional 145 | .storage/ 146 | www/ 147 | blueprints/ 148 | secrets.yaml 149 | scripts.yaml 150 | scenes.yaml 151 | home-assistant_v2.db 152 | home-assistant.log.1 153 | configuration.yaml 154 | automations.yaml 155 | .HA_VERSION 156 | home-assistant.log.fault 157 | home-assistant_v2.db-shm 158 | home-assistant_v2.db-wal 159 | -------------------------------------------------------------------------------- /custom_components/neerslag/__init__.py: -------------------------------------------------------------------------------- 1 | """The Neerslag Sensor (Buienalarm / Buienradar) integration.""" 2 | import asyncio 3 | from .load_frontend import setup_view 4 | import logging 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.core import HomeAssistant 8 | 9 | from .const import DOMAIN 10 | 11 | # TODO List the platforms that you want to support. 12 | # For your initial PR, limit it to 1 platform. 13 | PLATFORMS = ["sensor"] 14 | FRONTEND_SETUP_KEY = "frontend_setup_done" 15 | 16 | _LOGGER = logging.getLogger(__name__) 17 | 18 | 19 | async def options_update_listener( 20 | hass: HomeAssistant, config_entry: ConfigEntry 21 | ) -> None: 22 | """Handle options update. 23 | 24 | Wordt aangeroepen als de OptionsFlow een wijziging opslaat. 25 | Kopieert hier de options naar data, 26 | """ 27 | _LOGGER.info( 28 | "Neerslag: options_update_listener triggered for entry %s", 29 | config_entry.entry_id, 30 | ) 31 | 32 | # LET OP: dit gedrag is bewust zo gelaten; spiegel options terug naar data. 33 | hass.config_entries.async_update_entry( 34 | config_entry, 35 | data=config_entry.options, 36 | ) 37 | 38 | async def _ensure_frontend_setup(hass: HomeAssistant): 39 | """Run setup_view exactly once.""" 40 | hass.data.setdefault(DOMAIN, {}) 41 | 42 | if not hass.data[DOMAIN].get(FRONTEND_SETUP_KEY): 43 | await setup_view(hass) 44 | hass.data[DOMAIN][FRONTEND_SETUP_KEY] = True 45 | 46 | async def async_setup(hass: HomeAssistant, config: dict): 47 | """Set up the Neerslag Sensor (Buienalarm / Buienradar) component. (YAML path)""" 48 | await _ensure_frontend_setup(hass) 49 | return True 50 | 51 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): 52 | """Set up from a config entry (UI path).""" 53 | 54 | await _ensure_frontend_setup(hass) 55 | 56 | hass_data = dict(config_entry.data) 57 | unsub = config_entry.add_update_listener(options_update_listener) 58 | hass_data["unsub_options_update_listener"] = unsub 59 | 60 | hass.data.setdefault(DOMAIN, {}) 61 | hass.data[DOMAIN][config_entry.entry_id] = hass_data 62 | 63 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 64 | return True 65 | 66 | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): 67 | # Remove options_update_listener. 68 | 69 | _LOGGER.info( 70 | "Neerslag: unloading config entry %s", 71 | config_entry.entry_id, 72 | ) 73 | 74 | """Unload a config entry.""" 75 | unload_ok = all( 76 | await asyncio.gather( 77 | *[ 78 | hass.config_entries.async_forward_entry_unload(config_entry, platform) 79 | for platform in PLATFORMS 80 | ] 81 | ) 82 | ) 83 | 84 | # Options-listener verwijderen 85 | entry_data = hass.data[DOMAIN].get(config_entry.entry_id) 86 | if entry_data and "unsub_options_update_listener" in entry_data: 87 | try: 88 | entry_data["unsub_options_update_listener"]() 89 | except Exception: # defensief 90 | _LOGGER.exception( 91 | "Neerslag: error while unsubscribing options listener for %s", 92 | config_entry.entry_id, 93 | ) 94 | 95 | if unload_ok: 96 | hass.data[DOMAIN].pop(config_entry.entry_id) 97 | 98 | return unload_ok 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) 2 | # Neerslag App 3 | Neerslag app for Home Assistant. All-in-one package (Sensors + Card). 4 | 5 | Display rain forecast using Buienalarm and/or Buienradar sensor data. The Neerslag App (and the sensors) is fully configurable via the Home Assistant interface. 6 | 7 | > This package contains the Neerslag Card. Make sure to uninstall the Neerslag Card package from Home Assistant before using the Neerslag App to avoid unexpected behaviour. This includes manually removing the custom sensors. 8 | 9 | ## Features 10 | * Everything from the Neerslag Card; 11 | * Built-in Buienalarm and Buienradar sensors; 12 | * Ability to configure this app via the GUI; 13 | * Can use build-in Home Assistant configured location. 14 | 15 | ![Example](https://github.com/aex351/home-assistant-neerslag-app/raw/main/documentation/example.png) 16 | 17 | ## Installation overview 18 | 1) Install via HACS or manual; 19 | 2) Configure the Neerslag App (via interface); 20 | 3) Add the Neerslag Card to your dashboard. 21 | 22 | 23 | ## 1a. Install via HACS (recommended) 24 | This is the recommended option and also allows for easy updates. 25 | 1) Find this repository in HACS and click install 26 | 2) Restart Home Assistant and clear the browser cache; 27 | 3) Add the Neerslag App as an Integration in Home Assistant `(menu: settings -> devices & services -> add integration)`; 28 | 4) Restart Home Assistant and clear the browser cache (optional). 29 | 30 | For updates go to the Community Store (HACS) and click update. 31 | 32 | ## 1b. Manual install 33 | Not recommended, you will need to track updates manually by browsing to the repository; 34 | 1) Download the latest release of the Neerslag App from this repository; 35 | 2) In Home Assistant, create a folder `config/custom_components`; 36 | 3) Add the Neerslag App to the `custom_components` folder; 37 | 4) Restart Home Assistant; 38 | 5) Add the Neerslag App as an Integration in Home Assistant `(menu: settings -> devices & services -> add integration)`; 39 | 6) Restart Home Assistant and clear the browser cache (optional). 40 | 41 | For updates, repeat step 1 to 4. Home Assistant will not delete any configuration. 42 | 43 | ## 2. Configure the Neerslag App (via interface) 44 | The Neerslag App is fully configurable via the interface. 45 | 1) Go to `(menu: settings -> devices & services -> add integration)` and click on `configure`. 46 | 2) Select which sensor you want to use and provide the location data. There is an option to use the built-in Home Assistant location data. If this checkbox is selected, it will override the location settings of the individual sensors. 47 | 48 | ## 3. Add the Neerslag Card to your Dashboard 49 | 1) Go to your dashboard, go to `configure UI`; 50 | 2) Click `add card`; 51 | 3) Find the Neerslag Card in the list of cards; 52 | 4) Add the card and configure the card. 53 | 54 | > Note: Due to caching, The Neerslag Card might not be visible in the Home Assistant card selector directly after installing the Neerslag App. Restart Home Assistant and clear the browser cache to resolve this. 55 | 56 | ### Using one sensor: 57 | ```yaml 58 | type: 'custom:neerslag-card' 59 | title: Neerslag 60 | entity: sensor.neerslag_buienalarm_regen_data 61 | ``` 62 | ### Using two sensors: 63 | ```yaml 64 | type: 'custom:neerslag-card' 65 | title: Neerslag 66 | entities: 67 | - sensor.neerslag_buienalarm_regen_data 68 | - sensor.neerslag_buienradar_regen_data 69 | ``` 70 | > Note: If Home Assistant has not yet received data from the sensors, the card can remain blank. 71 | 72 | ### Advanced configuration options: 73 | Enable auto zoom to have the graph dynamically zoom in or out depending on the amount of rainfall. 74 | 75 | Note: By default auto zoom is disabled. Which gives the graph a fixed starting position displaying low, medium and heavy rainfall. Auto zoom will continue on extreme rainfall. Before version 2022.07.07.1 this setting was set to true. 76 | 77 | ```yaml 78 | autozoom: false 79 | ``` -------------------------------------------------------------------------------- /custom_components/neerslag/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Neerslag Sensor (Buienalarm / Buienradar) integration.""" 2 | import logging 3 | from typing import Optional 4 | 5 | import voluptuous as vol 6 | from homeassistant.core import callback 7 | 8 | from homeassistant import config_entries, core, exceptions 9 | from homeassistant.config_entries import ConfigEntry 10 | 11 | 12 | from .const import DOMAIN # pylint:disable=unused-import 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | # TODO adjust the data schema to the data that you need 17 | STEP_USER_DATA_SCHEMA = vol.Schema({vol.Optional("buienalarm", default=False): bool, 18 | vol.Optional("buienalarmLatitude", description={"suggested_value": "55.000"}): str, 19 | vol.Optional("buienalarmLongitude", description={"suggested_value": "5.000"}): str, 20 | vol.Optional("buienradar", default=False): bool, 21 | vol.Optional("buienradarLatitude", description={"suggested_value": "55.00"}): str, 22 | vol.Optional("buienradarLongitude", description={"suggested_value": "5.00"}): str, 23 | vol.Optional("NeerslagSensorUseHAforLocation", default=True): bool 24 | }) 25 | 26 | 27 | async def validate_input(hass: core.HomeAssistant, data): 28 | """Validate the user input allows us to connect. 29 | 30 | Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. 31 | """ 32 | # TODO validate the data can be used to set up a connection. 33 | 34 | # If your PyPI package is not built with async, pass your methods 35 | # to the executor: 36 | # await hass.async_add_executor_job( 37 | # your_validate_func, data["username"], data["password"] 38 | # ) 39 | 40 | # hub = PlaceholderHub(data["host"]) 41 | 42 | # if not await hub.authenticate(data["username"], data["password"]): 43 | # raise InvalidAuth 44 | 45 | # If you cannot connect: 46 | # throw CannotConnect 47 | # If the authentication is wrong: 48 | # InvalidAuth 49 | 50 | # Return info that you want to store in the config entry. 51 | return {"title": "Neerslag"} 52 | 53 | 54 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 55 | """Handle a config flow for Neerslag Sensor (Buienalarm / Buienradar).""" 56 | 57 | VERSION = 1 58 | # TODO pick one of the available connection classes in homeassistant/config_entries.py 59 | CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN 60 | 61 | async def async_step_user(self, user_input=None): 62 | """Handle the initial step.""" 63 | 64 | if self._async_current_entries(): 65 | return self.async_abort(reason="single_instance_allowed") 66 | if self.hass.data.get(DOMAIN): 67 | return self.async_abort(reason="single_instance_allowed") 68 | 69 | if user_input is None: 70 | return self.async_show_form( 71 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA 72 | ) 73 | 74 | errors = {} 75 | 76 | try: 77 | # info = await validate_input(self.hass, user_input) 78 | title = "Neerslag App" 79 | data = user_input 80 | # _LOGGER.info("Dit wordt nu uitgevoerd...........") 81 | # _LOGGER.info(data) 82 | 83 | except CannotConnect: 84 | errors["base"] = "cannot_connect" 85 | except InvalidAuth: 86 | errors["base"] = "invalid_auth" 87 | except Exception: # pylint: disable=broad-except 88 | _LOGGER.exception("Unexpected exception") 89 | errors["base"] = "unknown" 90 | else: 91 | return self.async_create_entry(title=title, data=data) 92 | 93 | return self.async_show_form( 94 | step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors 95 | ) 96 | 97 | @staticmethod 98 | @callback 99 | def async_get_options_flow(config_entry: ConfigEntry) -> config_entries.OptionsFlow: 100 | """Create the options flow.""" 101 | return OptionsFlowHandler() 102 | 103 | 104 | class OptionsFlowHandler(config_entries.OptionsFlow): 105 | """Handle Neerslag options.""" 106 | 107 | def __init__(self) -> None: 108 | """Initialize options flow.""" 109 | # eigen state kun je die hier initialiseren 110 | pass 111 | 112 | async def async_step_init(self, user_input=None): 113 | 114 | # _LOGGER.info("HIER>>>") 115 | # _LOGGER.info(self.config_entry.data.get("buienalarmLatitude")) 116 | # _LOGGER.info(self.config_entry.data.get("buienalarm")) 117 | # _LOGGER.info(self.config_entry.data.get("NeerslagSensorUseHAforLocation")) 118 | testtest = vol.Schema({vol.Optional("buienalarm", default=self.config_entry.data.get("buienalarm")): bool, 119 | vol.Optional("buienalarmLatitude", default=self.config_entry.data.get("buienalarmLatitude")): str, 120 | vol.Optional("buienalarmLongitude", default=self.config_entry.data.get("buienalarmLongitude")): str, 121 | vol.Optional("buienradar", default=self.config_entry.data.get("buienradar")): bool, 122 | vol.Optional("buienradarLatitude", default=self.config_entry.data.get("buienradarLatitude")): str, 123 | vol.Optional("buienradarLongitude", default=self.config_entry.data.get("buienradarLongitude")): str, 124 | vol.Optional("NeerslagSensorUseHAforLocation", default=self.config_entry.data.get("NeerslagSensorUseHAforLocation")): bool 125 | }) 126 | 127 | # _LOGGER.info("----->>>>---------------") 128 | # _LOGGER.info(self.config_entry.options) 129 | # _LOGGER.info(self.config_entry.data) 130 | # _LOGGER.info("------<<<<--------------") 131 | """Manage the options.""" 132 | if user_input is not None: 133 | # _LOGGER.info(user_input) 134 | # _LOGGER.info("<><><><><><><><><><>") 135 | # self.config_entry.data = user_input 136 | return self.async_create_entry(title="", data=user_input) 137 | 138 | return self.async_show_form( 139 | step_id="init", 140 | data_schema=testtest, 141 | ) 142 | 143 | 144 | class CannotConnect(exceptions.HomeAssistantError): 145 | """Error to indicate we cannot connect.""" 146 | 147 | 148 | class InvalidAuth(exceptions.HomeAssistantError): 149 | """Error to indicate there is invalid auth.""" 150 | -------------------------------------------------------------------------------- /custom_components/neerslag/sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import math 5 | import json 6 | from datetime import timedelta 7 | from random import random 8 | import random as rand 9 | from typing import Any 10 | 11 | import aiohttp 12 | from homeassistant.helpers.entity import Entity 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.core import HomeAssistant 15 | 16 | 17 | _LOGGER = logging.getLogger(__name__) 18 | 19 | SCAN_INTERVAL = timedelta(seconds=180) 20 | 21 | 22 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities): 23 | """Set up sensor entity.""" 24 | 25 | # Buienalarm – altijd entity registreren, enabled via checkbox 26 | buienalarm_enabled = bool(config_entry.data.get("buienalarm", False)) 27 | async_add_entities( 28 | [NeerslagSensorBuienalarm(hass, config_entry, buienalarm_enabled)], 29 | update_before_add=buienalarm_enabled, 30 | ) 31 | 32 | # Buienradar – idem 33 | buienradar_enabled = bool(config_entry.data.get("buienradar", False)) 34 | async_add_entities( 35 | [NeerslagSensorBuienradar(hass, config_entry, buienradar_enabled)], 36 | update_before_add=buienradar_enabled, 37 | ) 38 | 39 | 40 | class mijnBasis(Entity): 41 | _enabled : bool 42 | _unique_id : str | None 43 | _name : str | None 44 | _lat: float 45 | _lon: float 46 | 47 | def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, enabled: bool): 48 | _LOGGER.info("Neerslag entity geladen") 49 | 50 | async def mine_update_listener(self, hass: HomeAssistant, config_entry: ConfigEntry, pp=None): 51 | """Handle options update.""" 52 | 53 | if(self._name == "neerslag_buienalarm_regen_data"): 54 | self._enabled = bool(config_entry.data.get("buienalarm")) 55 | 56 | if(self._name == "neerslag_buienradar_regen_data"): 57 | self._enabled = bool(config_entry.data.get("buienradar")) 58 | 59 | @property 60 | def device_info(self): 61 | return { 62 | "identifiers": { 63 | # Serial numbers are unique identifiers within a specific domain 64 | ("neerslag", "neerslag-device") 65 | }, 66 | "name": "Neerslag App", 67 | "manufacturer": "aex351", 68 | "model": "All-in-one package", 69 | "sw_version": "", 70 | } 71 | 72 | @property 73 | def available(self) -> bool: 74 | """Return True if entity is available.""" 75 | return self._enabled 76 | 77 | @ property 78 | def state(self): 79 | return self._state 80 | 81 | @ property 82 | def name(self): 83 | return self._name 84 | 85 | @ property 86 | def unique_id(self): 87 | """Return unique ID.""" 88 | return self._unique_id 89 | 90 | async def async_update(self): 91 | self._state = random() 92 | return True 93 | 94 | class NeerslagSensorBuienalarm(mijnBasis): 95 | def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, enabled: bool): 96 | super().__init__(hass=hass, config_entry=config_entry, enabled=enabled) 97 | 98 | self._name = "neerslag_buienalarm_regen_data" 99 | self._state = "working" # None 100 | self._attrs = ["data empty"] 101 | self._unique_id = "neerslag-sensor-buienalarm-1" 102 | 103 | self._enabled = enabled 104 | config_entry.add_update_listener(self.mine_update_listener) 105 | 106 | if config_entry.data.get("NeerslagSensorUseHAforLocation") == True: 107 | self._lat = float(hass.config.latitude) 108 | self._lon = float(hass.config.longitude) 109 | 110 | else: 111 | self._lat = float(config_entry.data.get("buienalarmLatitude") or 0.0) 112 | self._lon = float(config_entry.data.get("buienalarmLongitude") or 0.0) 113 | 114 | # format values, enforce 3 decimals 115 | self._lat = float(f'{float(self._lat):.3f}' or 0.0) 116 | self._lon = float(f'{float(self._lon):.3f}' or 0.0) 117 | 118 | # self._entity_picture = "https://www.buienalarm.nl/assets/img/social.png" 119 | self._icon = "mdi:weather-cloudy" 120 | 121 | 122 | @ property 123 | def icon(self): 124 | return self._icon 125 | 126 | @ property 127 | def state_attributes(self): 128 | if not len(self._attrs): 129 | return 130 | return self._attrs 131 | # return {"data": self._attrs} 132 | 133 | async def async_update(self): 134 | if(self._enabled == True): 135 | self._state = random() 136 | self._attrs = await self.getBuienalarmData() 137 | return True 138 | 139 | async def getBuienalarmData(self): 140 | # Oude structuur 141 | data = { 142 | "data": { 143 | "success": False, 144 | "start": None, 145 | "delta": 0, 146 | "precip": [], 147 | } 148 | } 149 | 150 | try: 151 | timeout = aiohttp.ClientTimeout(total=5) 152 | async with aiohttp.ClientSession() as session: 153 | url = ( 154 | "https://imn-rust-lb.infoplaza.io/v4/nowcast/ba/timeseries/" 155 | + str(self._lat) + "/" + str(self._lon) 156 | + "/?c=" + str(rand.randint(0, 999999999999999)) 157 | ) 158 | 159 | async with session.get(url, timeout=timeout) as response: 160 | raw = await response.text() 161 | raw = raw.replace("\r\n", " ") 162 | 163 | if not raw.strip(): 164 | return data 165 | 166 | new_json = json.loads(raw) 167 | 168 | timeseries = new_json.get("data", []) 169 | summary = new_json.get("summary", {}) 170 | 171 | old = data["data"] 172 | old["success"] = True 173 | 174 | # START: uit summary.timestamp (of fallback) 175 | start_ts = summary.get("timestamp") 176 | if start_ts is None and timeseries: 177 | start_ts = timeseries[0].get("timestamp") 178 | old["start"] = start_ts 179 | 180 | # PRECIP-array 181 | old["precip"] = [ 182 | item.get("precipitationrate", 0) 183 | for item in timeseries 184 | ] 185 | 186 | # DELTA: interval tussen eerste twee timestamps 187 | if len(timeseries) >= 2: 188 | t0 = timeseries[0].get("timestamp") 189 | t1 = timeseries[1].get("timestamp") 190 | 191 | if isinstance(t0, int) and isinstance(t1, int): 192 | diff = t1 - t0 193 | if diff > 0: 194 | old["delta"] = diff # default wordt overschreven 195 | 196 | # --- PRECIP (mm/h → code 0..255) --- 197 | precip_codes = [] 198 | for item in timeseries: 199 | rate = item.get("precipitationrate", 0.0) # mm/uur (waarschijnlijk) 200 | 201 | if rate <= 0: 202 | code = 0 203 | else: 204 | code = 32 * math.log10(rate) + 109 205 | code = round(code) 206 | code = max(0, min(255, code)) # clamp 207 | 208 | precip_codes.append(code) 209 | 210 | old["precip"] = precip_codes 211 | 212 | except Exception: 213 | _LOGGER.info("getBuienalarmData - timeout") 214 | pass 215 | 216 | return data 217 | 218 | 219 | class NeerslagSensorBuienradar(mijnBasis): 220 | def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, enabled: bool): 221 | super().__init__(hass=hass, config_entry=config_entry, enabled=enabled) 222 | 223 | self._name = "neerslag_buienradar_regen_data" 224 | self._state = "working" # None 225 | self._attrs = ["data empty"] 226 | self._unique_id = "neerslag-sensor-buienradar-1" 227 | 228 | self._enabled = enabled 229 | config_entry.add_update_listener(self.mine_update_listener) 230 | 231 | if config_entry.data.get("NeerslagSensorUseHAforLocation") == True: 232 | self._lat = hass.config.latitude 233 | self._lon = hass.config.longitude 234 | 235 | else: 236 | self._lat = float(config_entry.data.get("buienradarLatitude") or 0.0) 237 | self._lon = float(config_entry.data.get("buienradarLongitude") or 0.0) 238 | 239 | # format values, enforce 2 decimals 240 | self._lat = float(f'{float(self._lat):.2f}' or 0.0) 241 | self._lon = float(f'{float(self._lon):.2f}' or 0.0) 242 | 243 | # self._entity_picture = "https://cdn.buienradar.nl/resources/images/br-logo-square.png" 244 | self._icon = "mdi:weather-cloudy" 245 | 246 | @ property 247 | def icon(self): 248 | return self._icon 249 | 250 | @ property 251 | def state_attributes(self): 252 | if not len(self._attrs): 253 | return 254 | return self._attrs 255 | # return {"data": self._attrs} 256 | 257 | async def async_update(self): 258 | if(self._enabled == True): 259 | self._state = random() 260 | self._attrs = await self.getBuienradarData() 261 | return True 262 | 263 | async def getBuienradarData(self) -> str: 264 | data = json.loads('{"data":""}') 265 | # return data 266 | try: 267 | timeout = aiohttp.ClientTimeout(total=5) 268 | async with aiohttp.ClientSession() as session: 269 | # https://www.buienradar.nl/overbuienradar/gratis-weerdata 270 | url = 'https://gps.buienradar.nl/getrr.php?lat=' + str(self._lat) + '&lon=' + str(self._lon) + '&c=' + str(rand.randint(0, 999999999999999)) 271 | # _LOGGER.info(url) 272 | async with session.get(url, timeout=timeout) as response: 273 | html = await response.text() 274 | dataRequest = ' '.join(html.splitlines()) 275 | if dataRequest == "" : 276 | dataRequest = "" 277 | data = json.loads('{"data": "' + dataRequest + '"}') 278 | # _LOGGER.info(data) 279 | await session.close() 280 | except: 281 | _LOGGER.info("getBuienradarData - timeout") 282 | pass 283 | 284 | return data 285 | --------------------------------------------------------------------------------