├── .gitignore ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── issue.md │ └── feature_request.md ├── workflows │ ├── validate.yml │ └── lint.yml └── stale.yml ├── requirements.txt ├── overview.png ├── hacs.json ├── custom_components └── wienerlinien │ ├── __init__.py │ ├── const.py │ ├── manifest.json │ └── sensor.py ├── .devcontainer ├── configuration.yaml ├── integration_start └── devcontainer.json ├── .vscode └── launch.json ├── .pre-commit-config.yaml ├── LICENSE ├── README.md └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tofuSCHNITZEL -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==22.3.0 2 | pre-commit==2.19.0 3 | -------------------------------------------------------------------------------- /overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tofuSCHNITZEL/home-assistant-wienerlinien/HEAD/overview.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wienerlinien", 3 | "render_readme": true, 4 | "hacs": "0.19.0", 5 | "homeassistant": "2022.3.1", 6 | "country": "AT" 7 | } 8 | -------------------------------------------------------------------------------- /custom_components/wienerlinien/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Home Assistant integration to get information about departures from a specified Wiener Linien stop. 3 | 4 | https://github.com/tofuSCHNITZEL/home-assistant-wienerlinien/ 5 | """ 6 | -------------------------------------------------------------------------------- /custom_components/wienerlinien/const.py: -------------------------------------------------------------------------------- 1 | """Constants""" 2 | BASE_URL = "http://www.wienerlinien.at/ogd_realtime/monitor?rbl={}" 3 | 4 | DEPARTURES = { 5 | "first": {"key": 0, "name": "{} first departure"}, 6 | "next": {"key": 1, "name": "{} next departure"}, 7 | } 8 | -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | logger: 3 | default: error 4 | logs: 5 | custom_components.wienerlinien: debug 6 | 7 | debugpy: 8 | 9 | sensor: 10 | - platform: wienerlinien 11 | stops: '4429' 12 | firstnext: first 13 | - platform: wienerlinien 14 | stops: '4429' 15 | firstnext: next 16 | - platform: wienerlinien 17 | firstnext: first 18 | stops: 19 | - '4429' 20 | - '3230' 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Version of the custom_component** 8 | 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **log** 14 | 15 | ``` 16 | Add your logs here. 17 | ``` 18 | -------------------------------------------------------------------------------- /custom_components/wienerlinien/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "wienerlinien", 3 | "name": "Wienerlinien", 4 | "version": "1.2.0", 5 | "documentation": "https://github.com/tofuSCHNITZEL/home-assistant-wienerlinien", 6 | "issue_tracker": "https://github.com/tofuSCHNITZEL/home-assistant-wienerlinien/issues", 7 | "dependencies": [], 8 | "codeowners": [ 9 | "@tofuSCHNITZEL" 10 | ], 11 | "requirements": [], 12 | "iot_class": "cloud_polling" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Running Home Assistant", 6 | "type": "python", 7 | "request": "attach", 8 | "port": 5678, 9 | "processId": "${command:pickProcess}", 10 | "pathMappings": [ 11 | { 12 | "localRoot": "${workspaceFolder}", 13 | "remoteRoot": "." 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.devcontainer/integration_start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make the config dir 4 | mkdir -p /tmp/config 5 | 6 | 7 | # Symplink the custom_components dir 8 | if [ -d "/tmp/config/custom_components" ]; then 9 | rm -rf /tmp/config/custom_components 10 | fi 11 | ln -sf "${PWD}/custom_components" /tmp/config/custom_components 12 | 13 | # Symlink configuration.yaml 14 | if [ ! -f ".devcontainer/configuration.yaml" ]; then 15 | cp .devcontainer/sample_configuration.yaml .devcontainer/configuration.yaml 16 | fi 17 | ln -sf "${PWD}/.devcontainer/configuration.yaml" /tmp/config/configuration.yaml 18 | 19 | 20 | # Start Home Assistant 21 | hass -c /tmp/config -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '30 16 */3 * *' 8 | 9 | jobs: 10 | validate-hassfest: 11 | runs-on: "ubuntu-latest" 12 | name: "With hassfest" 13 | steps: 14 | - name: "Check out repository" 15 | uses: "actions/checkout@v2" 16 | 17 | - name: "Hassfest validation" 18 | uses: "home-assistant/actions/hassfest@master" 19 | 20 | validate-hacs: 21 | runs-on: "ubuntu-latest" 22 | name: "With HACS" 23 | steps: 24 | - name: "Check out repository" 25 | uses: "actions/checkout@v3" 26 | 27 | - name: "HACS validation" 28 | uses: "hacs/action@main" 29 | with: 30 | category: "integration" -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 14 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | #exemptLabels: 7 | # - pinned 8 | # - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: Stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wienerlinien development", 3 | "image": "ghcr.io/ludeeus/devcontainer/integration:stable", 4 | "context": "..", 5 | "postCreateCommand": "make init", 6 | "appPort": [ 7 | "9123:8123" 8 | ], 9 | "extensions": [ 10 | "ms-python.python", 11 | "github.vscode-pull-request-github", 12 | "ryanluker.vscode-coverage-gutters", 13 | "ms-python.vscode-pylance" 14 | ], 15 | "settings": { 16 | "files.eol": "\n", 17 | "editor.tabSize": 4, 18 | "terminal.integrated.shell.linux": "/bin/bash", 19 | "python.pythonPath": "/usr/bin/python3", 20 | "python.linting.pylintEnabled": true, 21 | "python.linting.enabled": true, 22 | "python.formatting.provider": "black", 23 | "editor.formatOnPaste": false, 24 | "editor.formatOnSave": true, 25 | "editor.formatOnType": true, 26 | "files.trimTrailingWhitespace": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v2.32.1 4 | hooks: 5 | - id: pyupgrade 6 | stages: [manual] 7 | args: 8 | - "--py37-plus" 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 22.3.0 12 | hooks: 13 | - id: black 14 | stages: [manual] 15 | args: 16 | - --safe 17 | - --quiet 18 | files: ^((custom_components|script|tests)/.+)?[^/]+\.py$ 19 | 20 | - repo: https://github.com/codespell-project/codespell 21 | rev: v2.1.0 22 | hooks: 23 | - id: codespell 24 | stages: [manual] 25 | args: 26 | - --quiet-level=2 27 | - --ignore-words-list=hass,ba,fo,mabe 28 | 29 | - repo: https://github.com/pre-commit/pre-commit-hooks 30 | rev: v4.2.0 31 | hooks: 32 | - id: check-json 33 | stages: [manual] 34 | - id: requirements-txt-fixer 35 | stages: [manual] 36 | - id: check-ast 37 | stages: [manual] 38 | - id: mixed-line-ending 39 | stages: [manual] 40 | args: 41 | - --fix=lf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - 2020 Joakim Sørensen @ludeeus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | checks: write 13 | contents: write 14 | 15 | jobs: 16 | matrix: 17 | runs-on: ubuntu-latest 18 | name: Run ${{ matrix.checks }} 19 | strategy: 20 | matrix: 21 | checks: 22 | - pyupgrade 23 | - black 24 | - codespell 25 | - check-json 26 | - requirements-txt-fixer 27 | - check-ast 28 | - mixed-line-ending 29 | steps: 30 | - name: Check out repository 31 | uses: actions/checkout@v3 32 | 33 | - name: Set up Python 34 | uses: actions/setup-python@v3 35 | id: python 36 | with: 37 | python-version: 3.9 38 | 39 | - name: Install pre-commit 40 | run: | 41 | python3 -m pip install pre-commit 42 | pre-commit install-hooks --config .pre-commit-config.yaml 43 | - name: Run the check (${{ matrix.checks }}) 44 | run: pre-commit run --hook-stage manual ${{ matrix.checks }} --all-files --config .pre-commit-config.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge)](https://github.com/hacs/integration) 2 | 3 | # Get information about next departures 4 | 5 | A sensor platform which allows you to get information about departures from a specified Wiener Linien stop. 6 | 7 | To get started install this with [HACS](https://hacs.xyz/) 8 | 9 | ## Example configuration.yaml 10 | 11 | ```yaml 12 | sensor: 13 | platform: wienerlinien 14 | firstnext: first 15 | stops: 16 | - '4429' 17 | - '3230' 18 | ``` 19 | 20 | ## Configuration variables 21 | 22 | key | description 23 | -- | -- 24 | **platform (Required)** | The platform name. 25 | **stops (Required)** | RBL stop ID's 26 | **firstnext (Optional)** | `first` or `next` departure. 27 | 28 | ## Sample overview 29 | 30 | ![Sample overview](overview.png) 31 | 32 | ## Notes 33 | 34 | You can find out the Stop ID (rbl number) thanks to [Matthias Bendel](https://github.com/mabe-at) [https://till.mabe.at/rbl/](https://till.mabe.at/rbl/) 35 | 36 | 37 | This platform is using the [Wienerlinien API](http://www.wienerlinien.at) API to get the information. 38 | 'Datenquelle: Stadt Wien – data.wien.gv.at' 39 | Lizenz (CC BY 3.0 AT) 40 | 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | HAS_APK := $(shell command -v apk 2>/dev/null) 3 | HAS_APT := $(shell command -v apt 2>/dev/null) 4 | 5 | help: ## Shows help message. 6 | @printf "\033[1m%s\033[36m %s\033[0m \n\n" "Development environment for" "wienerlinien"; 7 | @awk 'BEGIN {FS = ":.*##";} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m make %-25s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST); 8 | @echo 9 | 10 | init: homeassistant-install requirements 11 | 12 | requirements: 13 | ifdef HAS_APK 14 | apk add libxml2-dev libxslt-dev 15 | endif 16 | ifdef HAS_APT 17 | sudo apt update && sudo apt install libxml2-dev libxslt-dev 18 | endif 19 | python3 -m pip --disable-pip-version-check install -U setuptools wheel 20 | python3 -m pip --disable-pip-version-check install -r requirements.txt 21 | 22 | start: ## Start the HA with the integration 23 | @bash .devcontainer/integration_start; 24 | 25 | lint: ## Run linters 26 | pre-commit install-hooks --config .github/pre-commit-config.yaml; 27 | pre-commit run --hook-stage manual --all-files --config .github/pre-commit-config.yaml; 28 | 29 | update: ## Pull master from custom-components/wienerlinien 30 | git pull upstream master; 31 | 32 | homeassistant-install: ## Install the latest dev version of Home Assistant 33 | python3 -m pip --disable-pip-version-check install -U setuptools wheel 34 | python3 -m pip --disable-pip-version-check \ 35 | install --upgrade git+git://github.com/home-assistant/home-assistant.git@dev; 36 | 37 | homeassistant-update: homeassistant-install ## Alias for 'homeassistant-install' -------------------------------------------------------------------------------- /custom_components/wienerlinien/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | A integration that allows you to get information about next departure from specified stop. 3 | For more details about this component, please refer to the documentation at 4 | https://github.com/tofuSCHNITZEL/home-assistant-wienerlinien 5 | """ 6 | import logging 7 | from datetime import timedelta 8 | 9 | import async_timeout 10 | import homeassistant.helpers.config_validation as cv 11 | import voluptuous as vol 12 | from homeassistant.components.sensor import PLATFORM_SCHEMA 13 | from homeassistant.exceptions import PlatformNotReady 14 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 15 | from homeassistant.helpers.entity import Entity 16 | 17 | from custom_components.wienerlinien.const import BASE_URL, DEPARTURES 18 | 19 | CONF_STOPS = "stops" 20 | CONF_APIKEY = "apikey" 21 | CONF_FIRST_NEXT = "firstnext" 22 | 23 | SCAN_INTERVAL = timedelta(seconds=30) 24 | 25 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 26 | { 27 | vol.Optional(CONF_APIKEY): cv.string, 28 | vol.Optional(CONF_STOPS, default=None): vol.All(cv.ensure_list, [cv.string]), 29 | vol.Optional(CONF_FIRST_NEXT, default="first"): cv.string, 30 | } 31 | ) 32 | 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | 37 | async def async_setup_platform(hass, config, add_devices_callback, discovery_info=None): 38 | """Setup.""" 39 | stops = config.get(CONF_STOPS) 40 | firstnext = config.get(CONF_FIRST_NEXT) 41 | dev = [] 42 | for stopid in stops: 43 | api = WienerlinienAPI(async_create_clientsession(hass), hass.loop, stopid) 44 | data = await api.get_json() 45 | try: 46 | name = data["data"]["monitors"][0]["locationStop"]["properties"]["title"] 47 | except Exception: 48 | raise PlatformNotReady() 49 | dev.append(WienerlinienSensor(api, name, firstnext)) 50 | add_devices_callback(dev, True) 51 | 52 | 53 | class WienerlinienSensor(Entity): 54 | """WienerlinienSensor.""" 55 | 56 | def __init__(self, api, name, firstnext): 57 | """Initialize.""" 58 | self.api = api 59 | self.firstnext = firstnext 60 | self._name = name 61 | self._state = None 62 | self.attributes = {} 63 | 64 | async def async_update(self): 65 | """Update data.""" 66 | try: 67 | data = await self.api.get_json() 68 | _LOGGER.debug(data) 69 | if data is None: 70 | return 71 | data = data.get("data", {}) 72 | except: 73 | _LOGGER.debug("Could not get new state") 74 | return 75 | 76 | if data is None: 77 | return 78 | try: 79 | line = data["monitors"][0]["lines"][0] 80 | departure = line["departures"]["departure"][ 81 | DEPARTURES[self.firstnext]["key"] 82 | ] 83 | if "timeReal" in departure["departureTime"]: 84 | self._state = departure["departureTime"]["timeReal"] 85 | elif "timePlanned" in departure["departureTime"]: 86 | self._state = departure["departureTime"]["timePlanned"] 87 | else: 88 | self._state = self._state 89 | 90 | self.attributes = { 91 | "destination": line["towards"], 92 | "platform": line["platform"], 93 | "direction": line["direction"], 94 | "name": line["name"], 95 | "countdown": departure["departureTime"]["countdown"], 96 | } 97 | except Exception: 98 | pass 99 | 100 | @property 101 | def name(self): 102 | """Return name.""" 103 | return DEPARTURES[self.firstnext]["name"].format(self._name) 104 | 105 | @property 106 | def state(self): 107 | """Return state.""" 108 | if self._state is None: 109 | return self._state 110 | else: 111 | return f"{self._state[:-2]}:{self._state[26:]}" 112 | 113 | @property 114 | def icon(self): 115 | """Return icon.""" 116 | return "mdi:bus" 117 | 118 | @property 119 | def extra_state_attributes(self): 120 | """Return attributes.""" 121 | return self.attributes 122 | 123 | @property 124 | def device_class(self): 125 | """Return device_class.""" 126 | return "timestamp" 127 | 128 | 129 | class WienerlinienAPI: 130 | """Call API.""" 131 | 132 | def __init__(self, session, loop, stopid): 133 | """Initialize.""" 134 | self.session = session 135 | self.loop = loop 136 | self.stopid = stopid 137 | 138 | async def get_json(self): 139 | """Get json from API endpoint.""" 140 | value = None 141 | url = BASE_URL.format(self.stopid) 142 | try: 143 | async with async_timeout.timeout(10): 144 | response = await self.session.get(url) 145 | value = await response.json() 146 | except Exception: 147 | pass 148 | 149 | return value 150 | --------------------------------------------------------------------------------