├── mypy.ini ├── custom_components └── berlin_transport │ ├── mypy.ini │ ├── manifest.json │ ├── __init__.py │ ├── strings.json │ ├── translations │ ├── en.json │ └── de.json │ ├── const.py │ ├── departure.py │ ├── config_flow.py │ └── sensor.py ├── docs └── screenshots │ ├── stop-id-api.jpg │ └── timetable-card.jpg ├── hacs.json ├── .github └── workflows │ ├── hassfest.yml │ └── code-quality.yml ├── LICENSE.md ├── .gitignore ├── README.md └── .pylintrc /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True -------------------------------------------------------------------------------- /custom_components/berlin_transport/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | -------------------------------------------------------------------------------- /docs/screenshots/stop-id-api.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vas3k/home-assistant-berlin-transport/HEAD/docs/screenshots/stop-id-api.jpg -------------------------------------------------------------------------------- /docs/screenshots/timetable-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vas3k/home-assistant-berlin-transport/HEAD/docs/screenshots/timetable-card.jpg -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Berlin (BVG) and Brandenburg (VBB) transport", 3 | "hide_default_branch": false, 4 | "country": "DE", 5 | "homeassistant": "2023.9.0", 6 | "hacs": "1.0.0" 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yml: -------------------------------------------------------------------------------- 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@v4" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "berlin_transport", 3 | "name": "Berlin (BVG) and Brandenburg (VBB) transport", 4 | "codeowners": ["@vas3k"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/vas3k/home-assistant-berlin-transport", 7 | "integration_type": "service", 8 | "iot_class": "cloud_polling", 9 | "issue_tracker": "https://github.com/vas3k/home-assistant-berlin-transport/issues", 10 | "requirements": ["requests", "requests-cache"], 11 | "version": "0.1.1" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: Black, Pylint, MyPy 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | name: Checks 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-python@v5 11 | with: 12 | python-version: 3.x 13 | - run: pip install --upgrade pip 14 | - run: pip install black pylint mypy 15 | - run: black --exclude="custom_components/berlin_transport/const.py" --diff . 16 | - run: pylint $(git ls-files '*.py') 17 | - run: mypy --install-types --non-interactive $(git ls-files '*.py') 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vasily Zubarev 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 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/__init__.py: -------------------------------------------------------------------------------- 1 | """The Berlin (BVG) and Brandenburg (VBB) transport integration.""" 2 | from __future__ import annotations 3 | 4 | from homeassistant.core import HomeAssistant 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import Platform 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | from homeassistant.helpers.typing import ConfigType 9 | 10 | from .const import DOMAIN, SCAN_INTERVAL # noqa 11 | 12 | PLATFORMS = [Platform.SENSOR] 13 | 14 | 15 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 16 | """Set up a config entry.""" 17 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 18 | entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) 19 | return True 20 | 21 | 22 | async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: 23 | """Update listener, called when the config entry options are changed.""" 24 | await hass.config_entries.async_reload(entry.entry_id) 25 | 26 | 27 | def setup( 28 | hass: HomeAssistant, config: ConfigType # pylint: disable=unused-argument 29 | ) -> bool: 30 | return True 31 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Search Stop", 6 | "description": "Enter the Name of the Stop to find the Stop ID", 7 | "data": { 8 | "search": "Name of the Stop" 9 | } 10 | }, 11 | "stop": { 12 | "title": "Select Stop", 13 | "description": "Select the desired Stop", 14 | "data": { 15 | "name": "Name of the Stop", 16 | "selected_stop": "ID of the Stop" 17 | } 18 | }, 19 | "details": { 20 | "title": "Further Details", 21 | "description": "Add further details", 22 | "data": { 23 | "walking_time": "Walking time in minutes", 24 | "direction": "Filter departures by direction", 25 | "excluded_stops": "Exclude nearby stops with IDs", 26 | "show_official_line_colors": "Enable official VBB line colors", 27 | "duration": "Show departures for how many minutes?", 28 | "suburban": "Include S-Bahn", 29 | "subway": "Include U-Bahn", 30 | "tram": "Include Tram", 31 | "bus": "Include Bus", 32 | "ferry": "Include Ferry", 33 | "express": "Include IC/ICE", 34 | "regional": "Include RB/RE" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Search Stop", 6 | "description": "Enter stop name to find its ID", 7 | "data": { 8 | "search": "Stop name" 9 | } 10 | }, 11 | "stop": { 12 | "title": "Select Stop", 13 | "description": "Select your desired stop from the list of search results.", 14 | "data": { 15 | "name": "Stop name", 16 | "selected_stop": "Stop (name and ID)" 17 | } 18 | }, 19 | "details": { 20 | "title": "More Details", 21 | "description": "Additional settings", 22 | "data": { 23 | "walking_time": "Walking time to stop (minutes)", 24 | "direction": "Only show departures which pass or end at these destinations", 25 | "excluded_stops": "Ignore these nearby stop IDs", 26 | "show_official_line_colors": "Use official VBB line colors", 27 | "duration": "Departure time range (minutes)", 28 | "suburban": "Show S-Bahn", 29 | "subway": "Show U-Bahn", 30 | "tram": "Show Tram", 31 | "bus": "Show Bus", 32 | "ferry": "Show Ferry", 33 | "express": "Show IC/ICE", 34 | "regional": "Show RB/RE" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Haltestelle suchen", 6 | "description": "Name eingeben, um die Haltestellen-ID zu finden", 7 | "data": { 8 | "search": "Haltestellenname" 9 | } 10 | }, 11 | "stop": { 12 | "title": "Haltestelle auswählen", 13 | "description": "Wähle die gewünschte Haltestelle aus der Liste der Suchergebnisse aus.", 14 | "data": { 15 | "name": "Haltestellenname", 16 | "selected_stop": "Haltestelle (Name und ID)" 17 | } 18 | }, 19 | "details": { 20 | "title": "Weitere Details", 21 | "description": "Zusätzliche Einstellungen", 22 | "data": { 23 | "walking_time": "Gehminuten zur Haltestelle", 24 | "direction": "Nur Abfahrten zu diesen (End-)Haltestellen", 25 | "excluded_stops": "Diese nahegelegenen Haltestellen ignorieren", 26 | "show_official_line_colors": "Offizielle VBB-Farben verwenden", 27 | "duration": "Zeitraum für Abfahrten (Minuten)", 28 | "suburban": "S-Bahn anzeigen", 29 | "subway": "U-Bahn anzeigen", 30 | "tram": "Tram anzeigen", 31 | "bus": "Bus anzeigen", 32 | "ferry": "Fähren anzeigen", 33 | "express": "IC/ICE anzeigen", 34 | "regional": "RB/RE anzeigen" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/const.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | DOMAIN = "berlin_transport" 4 | SCAN_INTERVAL = timedelta(seconds=90) 5 | API_ENDPOINT = "https://v6.vbb.transport.rest" 6 | API_MAX_RESULTS = 15 7 | 8 | DEFAULT_ICON = "mdi:clock" 9 | 10 | CONF_DEPARTURES = "departures" 11 | CONF_DEPARTURES_NAME = "name" 12 | CONF_DEPARTURES_STOP_ID = "stop_id" 13 | CONF_SELECTED_STOP = "selected_stop" 14 | CONF_DEPARTURES_EXCLUDED_STOPS = "excluded_stops" 15 | CONF_DEPARTURES_WALKING_TIME = "walking_time" 16 | CONF_DEPARTURES_DIRECTION = "direction" 17 | CONF_DEPARTURES_DURATION = "duration" 18 | CONF_SHOW_API_LINE_COLORS = "show_official_line_colors" 19 | CONF_TYPE_SUBURBAN = "suburban" 20 | CONF_TYPE_SUBWAY = "subway" 21 | CONF_TYPE_TRAM = "tram" 22 | CONF_TYPE_BUS = "bus" 23 | CONF_TYPE_FERRY = "ferry" 24 | CONF_TYPE_EXPRESS = "express" 25 | CONF_TYPE_REGIONAL = "regional" 26 | 27 | TRANSPORT_TYPE_VISUALS = { 28 | CONF_TYPE_SUBURBAN: { 29 | "code": "S", 30 | "icon": "mdi:subway-variant", 31 | "color": "#008D4F", 32 | }, 33 | CONF_TYPE_SUBWAY: { 34 | "code": "U", 35 | "icon": "mdi:subway", 36 | "color": "#2864A6", 37 | }, 38 | CONF_TYPE_TRAM: { 39 | "code": "M", 40 | "icon": "mdi:tram", 41 | "color": "#D82020", 42 | }, 43 | CONF_TYPE_BUS: { 44 | "code": "BUS", 45 | "icon": "mdi:bus", 46 | "color": "#A5027D" 47 | }, 48 | CONF_TYPE_FERRY: { 49 | "code": "F", 50 | "icon": "mdi:ferry", 51 | "color": "#0080BA" 52 | }, 53 | CONF_TYPE_EXPRESS: { 54 | "code": "Train", 55 | "icon": "mdi:train", 56 | "color": "#4D4D4D" 57 | }, 58 | CONF_TYPE_REGIONAL: { 59 | "code": "RE", 60 | "icon": "mdi:train", 61 | "color": "#F01414" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/departure.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from .const import TRANSPORT_TYPE_VISUALS, DEFAULT_ICON 5 | 6 | 7 | @dataclass 8 | class Departure: 9 | """Departure dataclass to store data from API: 10 | https://v6.vbb.transport.rest/api.html#get-stopsiddepartures""" 11 | 12 | trip_id: str 13 | line_name: str 14 | line_type: str 15 | timestamp: datetime 16 | time: datetime 17 | direction: str | None = None 18 | icon: str | None = None 19 | bg_color: str | None = None 20 | fallback_color: str | None = None 21 | location: tuple[float, float] | None = None 22 | cancelled: bool = False 23 | delay: int | None = None 24 | 25 | @classmethod 26 | def from_dict(cls, source): 27 | line_type = source.get("line", {}).get("product") 28 | line_visuals = TRANSPORT_TYPE_VISUALS.get(line_type) or {} 29 | timestamp = datetime.fromisoformat( 30 | source.get("when") or source.get("plannedWhen") 31 | ) 32 | return cls( 33 | trip_id=source["tripId"], 34 | line_name=source.get("line", {}).get("name"), 35 | line_type=line_type, 36 | timestamp=timestamp, 37 | time=timestamp.strftime("%H:%M"), 38 | direction=source.get("direction"), 39 | icon=line_visuals.get("icon") or DEFAULT_ICON, 40 | bg_color=source.get("line", {}).get("color", {}).get("bg"), 41 | fallback_color=line_visuals.get("color"), 42 | location=[ 43 | source.get("currentTripPosition", {}).get("latitude") or 0.0, 44 | source.get("currentTripPosition", {}).get("longitude") or 0.0, 45 | ], 46 | cancelled=source.get("cancelled", False), 47 | delay=source.get("delay", None), 48 | ) 49 | 50 | def to_dict(self, show_api_line_colors: bool, walking_time: int): 51 | color = self.fallback_color 52 | if show_api_line_colors and self.bg_color is not None: 53 | color = self.bg_color 54 | return { 55 | "line_name": self.line_name, 56 | "line_type": self.line_type, 57 | "time": self.time, 58 | "timestamp": self.timestamp, 59 | "direction": self.direction, 60 | "color": color, 61 | "cancelled": self.cancelled, 62 | "delay": self.delay, 63 | "walking_time": walking_time, 64 | } 65 | 66 | # Make the object hashable and use all infos that can be displayed in the 67 | # frontend 68 | def __hash__(self): 69 | # The value of colors and walking time doesn't matter, it just needs to 70 | # be the same for all evaluations of this function 71 | items = self.to_dict(show_api_line_colors=False, walking_time=0).items() 72 | # Dictionaries are not hashable, so use the items, sort them for 73 | # reproducibility. Convert it to a tuple, since lists are also not 74 | # hashable 75 | return hash(tuple(sorted(items))) 76 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/config_flow.py: -------------------------------------------------------------------------------- 1 | # mypy: disable-error-code="attr-defined,call-arg" 2 | """The Berlin (BVG) and Brandenburg (VBB) transport integration.""" 3 | from __future__ import annotations 4 | 5 | import logging 6 | 7 | from typing import Any, Optional 8 | import requests 9 | import voluptuous as vol 10 | 11 | from homeassistant import config_entries 12 | from homeassistant.data_entry_flow import FlowResult 13 | import homeassistant.helpers.config_validation as cv 14 | from homeassistant.helpers import selector 15 | 16 | from .const import ( 17 | API_ENDPOINT, 18 | API_MAX_RESULTS, 19 | CONF_DEPARTURES_STOP_ID, 20 | CONF_DEPARTURES_NAME, 21 | CONF_SELECTED_STOP, 22 | CONF_DEPARTURES_DIRECTION, 23 | CONF_DEPARTURES_EXCLUDED_STOPS, 24 | CONF_DEPARTURES_DURATION, 25 | CONF_DEPARTURES_WALKING_TIME, 26 | CONF_SHOW_API_LINE_COLORS, 27 | DOMAIN, # noqa 28 | ) 29 | 30 | from .sensor import TRANSPORT_TYPES_SCHEMA 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | CONF_SEARCH = "search" 35 | CONF_FOUND_STOPS = "found_stops" 36 | 37 | DATA_SCHEMA = vol.Schema( 38 | { 39 | vol.Optional(CONF_DEPARTURES_DIRECTION): cv.string, 40 | vol.Optional(CONF_DEPARTURES_EXCLUDED_STOPS): cv.string, 41 | vol.Optional(CONF_DEPARTURES_DURATION): cv.positive_int, 42 | vol.Optional(CONF_DEPARTURES_WALKING_TIME, default=1): cv.positive_int, 43 | vol.Optional(CONF_SHOW_API_LINE_COLORS, default=False): cv.boolean, 44 | **TRANSPORT_TYPES_SCHEMA, 45 | } 46 | ) 47 | 48 | NAME_SCHEMA = vol.Schema( 49 | { 50 | vol.Required(CONF_SEARCH): cv.string, 51 | } 52 | ) 53 | 54 | # IPv6 is broken, see: https://github.com/public-transport/transport.rest/issues/20 55 | requests.packages.urllib3.util.connection.HAS_IPV6 = False 56 | 57 | 58 | def get_stop_id(name) -> Optional[list[dict[str, Any]]]: 59 | try: 60 | response = requests.get( 61 | url=f"{API_ENDPOINT}/locations", 62 | params={ 63 | "query": name, 64 | "results": API_MAX_RESULTS, 65 | }, 66 | timeout=30, 67 | ) 68 | response.raise_for_status() 69 | except requests.exceptions.HTTPError as ex: 70 | _LOGGER.warning(f"API error: {ex}") 71 | return [] 72 | except requests.exceptions.Timeout as ex: 73 | _LOGGER.warning(f"API timeout: {ex}") 74 | return [] 75 | 76 | _LOGGER.debug(f"OK: stops for {name}: {response.text}") 77 | 78 | # parse JSON response 79 | try: 80 | stops = response.json() 81 | except requests.exceptions.InvalidJSONError as ex: 82 | _LOGGER.error(f"API invalid JSON: {ex}") 83 | return [] 84 | 85 | # convert api data into objects 86 | return [ 87 | {CONF_DEPARTURES_NAME: stop["name"], CONF_DEPARTURES_STOP_ID: stop["id"]} 88 | for stop in stops 89 | if stop["type"] == "stop" 90 | ] 91 | 92 | 93 | def list_stops(stops) -> Optional[vol.Schema]: 94 | """Provides a drop down list of stops""" 95 | schema = vol.Schema( 96 | { 97 | vol.Required(CONF_SELECTED_STOP): selector.SelectSelector( 98 | selector.SelectSelectorConfig( 99 | options=[ 100 | f"{stop[CONF_DEPARTURES_NAME]} [{stop[CONF_DEPARTURES_STOP_ID]}]" 101 | for stop in stops 102 | ], 103 | mode=selector.SelectSelectorMode.DROPDOWN 104 | ) 105 | ) 106 | } 107 | ) 108 | 109 | return schema 110 | 111 | 112 | class TransportConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 113 | VERSION = 1 114 | 115 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 116 | 117 | def __init__(self) -> None: 118 | """Init the ConfigFlow.""" 119 | self.data: dict[str, Any] = {} 120 | 121 | async def async_step_user( 122 | self, user_input: dict[str, Any] | None = None 123 | ) -> FlowResult: 124 | """Handle the initial step.""" 125 | if user_input is None: 126 | return self.async_show_form( 127 | step_id="user", 128 | data_schema=NAME_SCHEMA, 129 | errors={}, 130 | ) 131 | self.data[CONF_FOUND_STOPS] = await self.hass.async_add_executor_job( 132 | get_stop_id, user_input[CONF_SEARCH] 133 | ) 134 | 135 | _LOGGER.debug( 136 | f"OK: found stops for {user_input[CONF_SEARCH]}: {self.data[CONF_FOUND_STOPS]}" 137 | ) 138 | 139 | return await self.async_step_stop() 140 | 141 | async def async_step_stop( 142 | self, user_input: dict[str, Any] | None = None 143 | ) -> FlowResult: 144 | """Handle the initial step.""" 145 | if user_input is None: 146 | return self.async_show_form( 147 | step_id="stop", 148 | data_schema=list_stops(self.data[CONF_FOUND_STOPS]), 149 | errors={}, 150 | ) 151 | 152 | selected_stop = next( 153 | (stop[CONF_DEPARTURES_NAME], stop[CONF_DEPARTURES_STOP_ID]) 154 | for stop in self.data[CONF_FOUND_STOPS] 155 | if user_input[CONF_SELECTED_STOP] 156 | == f"{stop[CONF_DEPARTURES_NAME]} [{stop[CONF_DEPARTURES_STOP_ID]}]" 157 | ) 158 | ( 159 | self.data[CONF_DEPARTURES_NAME], 160 | self.data[CONF_DEPARTURES_STOP_ID], 161 | ) = selected_stop 162 | _LOGGER.debug(f"OK: selected stop {selected_stop[0]} [{selected_stop[1]}]") 163 | 164 | return await self.async_step_details() 165 | 166 | async def async_step_details( 167 | self, user_input: dict[str, Any] | None = None 168 | ) -> FlowResult: 169 | """Handle the details.""" 170 | if user_input is None: 171 | return self.async_show_form( 172 | step_id="details", 173 | data_schema=DATA_SCHEMA, 174 | errors={}, 175 | ) 176 | 177 | data = user_input 178 | data[CONF_DEPARTURES_STOP_ID] = self.data[CONF_DEPARTURES_STOP_ID] 179 | data[CONF_DEPARTURES_NAME] = self.data[CONF_DEPARTURES_NAME] 180 | return self.async_create_entry( 181 | title=f"{data[CONF_DEPARTURES_NAME]} [{data[CONF_DEPARTURES_STOP_ID]}]", 182 | data=data, 183 | ) 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Berlin (BVG) and Brandenburg (VBB) transport widget for Home Assistant 2 | 3 | Custom integration that displays upcoming departures from your defined public transport stops for Berlin and Brandenburg. 4 | 5 | This repository contains only the integration, **the Lovelace card itself lives here: https://github.com/vas3k/lovelace-berlin-transport-card** 6 | 7 | You need to install them both. Preferably through HACS. We have separated two repositories to make installation through it more convenient. 8 | 9 | ![](./docs/screenshots/timetable-card.jpg) 10 | 11 | > I use [iOS Dark Mode Theme](https://github.com/basnijholt/lovelace-ios-dark-mode-theme) by @basnijholt, installed from [HACS](https://hacs.xyz/) 12 | 13 | ## 💿 Installation 14 | 15 | The component consists of two parts: 16 | 17 | 1. A sensor, which tracks departures via [VBB public API](https://v6.vbb.transport.rest/api.html#get-stopsiddepartures) every 90 seconds. This is this repository. 18 | 2. A widget (card) for the lovelace dashboard, which displays upcoming transport in a nice way. It has its own [separate repository](https://github.com/vas3k/lovelace-berlin-transport-card) with installation instructions. 19 | 20 | ### Install sensor component via HACS 21 | 22 | 1. Add this [repository](https://github.com/vas3k/home-assistant-berlin-transport) as a custom repository in HACS in the category "integration". 23 | 1. Add `Berlin (BVG) and Brandenburg (VBB) transport` as a new integration under `Settings` -> `Devices & services` 24 | 1. Search for your stop. Partial matches are supported — up to 15 relevant stops will be listed. 25 | 1. Select the stop you want to monitor. 26 | 1. (Optional) Configure additional parameters: 27 | - Direction: Use `stop_id` to filter departures by direction. Provide the stop_id of stop along the intended lines or their final destination. Multiple values can be specified using a comma-separated list. See [below](#how-do-i-find-my-stop_id) for how to find the `stop id`. 28 | - Exclude stops: List of `stop_id` which should be excluded. Use if BVG/VBB is returning departures from nearby stops. Multiple values can be specified using a comma-separated list. 29 | - Duration: Defines how many minutes into the future departures should be fetched. Default is 10 minutes. 30 | - Walking time: Enter the time needed to walk to the stop. This prevents unreachable departures from being shown. 31 | - Enable official VBB line colors: Optionally enable official VBB line colors. By default, predefined colors are used. 32 | - Transport options: Choose which transport types (e.g., bus, ferry) to show or hide. 33 | 1. Done. If you want to change options later on, just run through the steps again with the same stop. The previous entity will be overwritten automatically. 34 | 35 | ### Install sensor component manually 36 | 37 | #### How do I find my `stop_id`? 38 | 39 | Unfortunately, I didn't have time to figure out a proper user-friendly approach of adding new components to Home Assistant, so you will have to do some routine work of finding the IDs of the nearest transport stops to you. Sorry about that :) 40 | 41 | Simply use this URL: **https://v6.vbb.transport.rest/locations?results=1&query=alexanderplatz** 42 | 43 | Replace `alexanderplatz` with the name of your own stop. 44 | 45 | ![](./docs/screenshots/stop-id-api.jpg) 46 | 47 | > 🧐 **Pro tip:** 48 | > You can also use their [location-based API](https://v6.vbb.transport.rest/api.html#get-stopsnearby) to find all stops nearby using your GPS coordinates. 49 | 50 | **1.** Copy the whole [berlin_transport](./custom_components/) directory to the `custom_components` folder of your Home Assistant installation. If you can't find the `custom_components` directory at the same level with your `configuration.yml` — simply create it yourself and put `berlin_transport` there. 51 | 52 | **2.** Go to Home Assistant web interface -> `Developer Tools` -> `Check and Restart` and click "Restart" button. It will reload all components in the system. 53 | 54 | **3.** Now you can add your new custom sensor to the corresponding section in the `configuration.yml` file. 55 | 56 | ```yaml 57 | sensor: 58 | - platform: berlin_transport 59 | departures: 60 | - name: "S+U Schönhauser Allee" # free-form name, only for display purposes 61 | stop_id: 900110001 # actual Stop ID for the API 62 | # direction: 900110002,900007102 # Optional stop_id to limit departures for a specific direction (same URL as to find the stop_id), multiple Values can be specified using a comma separated list 63 | # walking_time: 5 # Optional parameter with value in minutes that hides transport closer than N minutes 64 | # suburban: false # Optionally hide transport options 65 | # show_official_line_colors: true # Optionally enable official VBB line colors. By default predefined colors will be used. 66 | # duration: 30 # Optional (default 10), query departures for how many minutes from now? 67 | - name: "Stargarder Str." # currently you have to add more than one stop to track 68 | stop_id: 900000110501 69 | # direction: 900000100002 # Optional stop_id to limit departures for a specific direction (same URL as to find the stop_id), multiple Values can be specified using a comma separated list 70 | # excluded_stops: 900110502,900007102 # Exclude these stop IDs from the departures, duplicate departures may be shown for nearby stations 71 | # walking_time: 5 # Optional parameter with value in minutes that hide transport closer than N minutes 72 | # show_official_line_colors: true # Optionally enable official VBB line colors. By default predefined colors will be used. 73 | # duration: 30 # Optional (default 10), query departures for how many minutes from now? 74 | ``` 75 | 76 | **4.** Restart Home Assistant core again and you should now see two new entities (however, it may take some time for them to fetch new data). If you don't see anything new — check the logs (Settings -> System -> Logs). Some error should pop up there. 77 | 78 | ### Add the lovelace card 79 | 80 | Go to [lovelace-berlin-transport-card](https://github.com/vas3k/lovelace-berlin-transport-card) repo and follow installation instructions there. 81 | 82 | ## 👩‍💻 Technical details 83 | 84 | This sensor uses VBB Public API to fetch all transport information. 85 | 86 | - API docs: https://v5.vbb.transport.rest/api.html 87 | - Rate limit: 100 req/min 88 | - Format: [HAFAS](https://github.com/public-transport/hafas-client) 89 | 90 | The component updates every 60-90 seconds, but it makes a separate request for each stop. That's usually enough, but I wouldn't recommend adding dozens of different stops so you don't hit the rate limit. 91 | 92 | The VBB API is a bit unstable (as you can guess), so sometimes it gives random 503 or Timeout errors. This is normal. I haven't found how to overcome this, but it doesn't cause any problems other than warning messages in the logs. 93 | 94 | After fetching the API, it creates one entity for each stop and writes 10 upcoming departures into `attributes.departures`. The entity state is not really used anywhere, it just shows the next departure in a human-readable format. If you have any ideas how to use it better — welcome to Github Issues. 95 | 96 | > 🤔 97 | > In principle, the HAFAS format is standardized in many other cities too, so you should have no problem adapting this component to more places if you wish. Check out [transport.rest](https://transport.rest/) for an inspiration. 98 | 99 | ## ❤️ Contributions 100 | 101 | Contributions are welcome. Feel free to [open a PR](https://github.com/vas3k/home-assistant-berlin-transport/pulls) and send it to review. If you are unsure, [open an Issue](https://github.com/vas3k/home-assistant-berlin-transport/issues) and ask for advice. 102 | 103 | ## 🐛 Bug reports and feature requests 104 | 105 | Since this is my small hobby project, I cannot guarantee you a 100% support or any help with configuring your dashboards. I hope for your understanding. 106 | 107 | - **If you find a bug** - open [an Issue](https://github.com/vas3k/home-assistant-berlin-transport/issues) and describe the exact steps to reproduce it. Attach screenshots, copy all logs and other details to help me find the problem. 108 | - **If you're missing a certain feature**, describe it in Issues and try to code it yourself. It's not hard. At the very least, you can try to [bribe me with a PayPal donation](https://www.paypal.com/paypalme/vas3kcom) to make the feature just for you :) 109 | 110 | ## 👮‍♀️ License 111 | 112 | - [MIT](./LICENSE.md) 113 | -------------------------------------------------------------------------------- /custom_components/berlin_transport/sensor.py: -------------------------------------------------------------------------------- 1 | # mypy: disable-error-code="attr-defined" 2 | 3 | """The Berlin (BVG) and Brandenburg (VBB) transport integration.""" 4 | from __future__ import annotations 5 | import logging 6 | from datetime import datetime, timedelta 7 | 8 | from requests.exceptions import HTTPError, InvalidJSONError, Timeout 9 | from requests_cache import CachedSession 10 | import voluptuous as vol 11 | 12 | from homeassistant.core import HomeAssistant 13 | import homeassistant.helpers.config_validation as cv 14 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 15 | from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType 16 | from homeassistant.config_entries import ConfigEntry 17 | from homeassistant.components.sensor import SensorEntity 18 | from homeassistant.components.sensor import PLATFORM_SCHEMA 19 | 20 | from .const import ( # pylint: disable=unused-import 21 | DOMAIN, # noqa 22 | SCAN_INTERVAL, # noqa 23 | API_ENDPOINT, 24 | API_MAX_RESULTS, 25 | CONF_DEPARTURES, 26 | CONF_DEPARTURES_DIRECTION, 27 | CONF_DEPARTURES_EXCLUDED_STOPS, 28 | CONF_DEPARTURES_DURATION, 29 | CONF_DEPARTURES_STOP_ID, 30 | CONF_DEPARTURES_WALKING_TIME, 31 | CONF_SHOW_API_LINE_COLORS, 32 | CONF_TYPE_BUS, 33 | CONF_TYPE_EXPRESS, 34 | CONF_TYPE_FERRY, 35 | CONF_TYPE_REGIONAL, 36 | CONF_TYPE_SUBURBAN, 37 | CONF_TYPE_SUBWAY, 38 | CONF_TYPE_TRAM, 39 | CONF_DEPARTURES_NAME, 40 | DEFAULT_ICON, 41 | ) 42 | from .departure import Departure 43 | 44 | _LOGGER = logging.getLogger(__name__) 45 | 46 | TRANSPORT_TYPES_SCHEMA = { 47 | vol.Optional(CONF_TYPE_SUBURBAN, default=True): cv.boolean, 48 | vol.Optional(CONF_TYPE_SUBWAY, default=True): cv.boolean, 49 | vol.Optional(CONF_TYPE_TRAM, default=True): cv.boolean, 50 | vol.Optional(CONF_TYPE_BUS, default=True): cv.boolean, 51 | vol.Optional(CONF_TYPE_FERRY, default=True): cv.boolean, 52 | vol.Optional(CONF_TYPE_EXPRESS, default=True): cv.boolean, 53 | vol.Optional(CONF_TYPE_REGIONAL, default=True): cv.boolean, 54 | } 55 | 56 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 57 | { 58 | vol.Optional(CONF_DEPARTURES): [ 59 | { 60 | vol.Required(CONF_DEPARTURES_NAME): cv.string, 61 | vol.Required(CONF_DEPARTURES_STOP_ID): cv.positive_int, 62 | vol.Optional(CONF_DEPARTURES_DIRECTION): cv.string, 63 | vol.Optional(CONF_DEPARTURES_EXCLUDED_STOPS): cv.string, 64 | vol.Optional(CONF_DEPARTURES_DURATION): cv.positive_int, 65 | vol.Optional(CONF_DEPARTURES_WALKING_TIME, default=1): cv.positive_int, 66 | vol.Optional(CONF_SHOW_API_LINE_COLORS, default=False): cv.boolean, 67 | **TRANSPORT_TYPES_SCHEMA, 68 | } 69 | ] 70 | } 71 | ) 72 | 73 | 74 | async def async_setup_platform( 75 | hass: HomeAssistant, 76 | config: ConfigType, 77 | async_add_entities: AddEntitiesCallback, 78 | _: DiscoveryInfoType | None = None, 79 | ) -> None: 80 | """Set up the sensor platform.""" 81 | if CONF_DEPARTURES in config: 82 | for departure in config[CONF_DEPARTURES]: 83 | async_add_entities([TransportSensor(hass, departure)]) 84 | 85 | 86 | async def async_setup_entry( 87 | hass: HomeAssistant, 88 | config_entry: ConfigEntry, 89 | async_add_entities: AddEntitiesCallback, 90 | ) -> None: 91 | async_add_entities([TransportSensor(hass, config_entry.data)]) 92 | 93 | 94 | class TransportSensor(SensorEntity): 95 | departures: list[Departure] = [] 96 | 97 | def __init__(self, hass: HomeAssistant, config: dict) -> None: 98 | self.hass: HomeAssistant = hass 99 | self.config: dict = config 100 | self.stop_id: int = config[CONF_DEPARTURES_STOP_ID] 101 | self.excluded_stops: str | None = config.get(CONF_DEPARTURES_EXCLUDED_STOPS) 102 | self.sensor_name: str | None = config.get(CONF_DEPARTURES_NAME) 103 | self.direction: str | None = config.get(CONF_DEPARTURES_DIRECTION) 104 | self.duration: int | None = config.get(CONF_DEPARTURES_DURATION) 105 | self.walking_time: int = config.get(CONF_DEPARTURES_WALKING_TIME) or 1 106 | # we add +1 minute anyway to delete the "just gone" transport 107 | self.show_api_line_colors: bool = config.get(CONF_SHOW_API_LINE_COLORS) or False 108 | self.session: CachedSession = CachedSession( 109 | backend='memory', 110 | cache_control=True, 111 | expire_after=timedelta(days=1) 112 | ) 113 | 114 | @property 115 | def name(self) -> str: 116 | return self.sensor_name or f"Stop ID: {self.stop_id}" 117 | 118 | @property 119 | def icon(self) -> str: 120 | next_departure = self.next_departure() 121 | if next_departure: 122 | return next_departure.icon 123 | return DEFAULT_ICON 124 | 125 | @property 126 | def unique_id(self) -> str: 127 | return f"stop_{self.stop_id}_{self.sensor_name}_departures" 128 | 129 | @property 130 | def state(self) -> str: 131 | next_departure = self.next_departure() 132 | if next_departure: 133 | return f"Next {next_departure.line_name} at {next_departure.time}" 134 | return "N/A" 135 | 136 | @property 137 | def extra_state_attributes(self): 138 | return { 139 | "departures": [ 140 | departure.to_dict(self.show_api_line_colors, self.walking_time) 141 | for departure in self.departures or [] 142 | ] 143 | } 144 | 145 | def update(self): 146 | self.departures = self.fetch_departures() 147 | 148 | def fetch_directional_departure(self, direction: str | None) -> list[Departure]: 149 | try: 150 | response = self.session.get( 151 | url=f"{API_ENDPOINT}/stops/{self.stop_id}/departures", 152 | params={ 153 | "when": ( 154 | datetime.utcnow() + timedelta(minutes=self.walking_time) 155 | ).isoformat(), 156 | "direction": direction, 157 | "duration": self.duration, 158 | "results": API_MAX_RESULTS, 159 | "suburban": self.config.get(CONF_TYPE_SUBURBAN) or False, 160 | "subway": self.config.get(CONF_TYPE_SUBWAY) or False, 161 | "tram": self.config.get(CONF_TYPE_TRAM) or False, 162 | "bus": self.config.get(CONF_TYPE_BUS) or False, 163 | "ferry": self.config.get(CONF_TYPE_FERRY) or False, 164 | "express": self.config.get(CONF_TYPE_EXPRESS) or False, 165 | "regional": self.config.get(CONF_TYPE_REGIONAL) or False, 166 | }, 167 | timeout=30, 168 | ) 169 | response.raise_for_status() 170 | except HTTPError as ex: 171 | _LOGGER.warning(f"API error: {ex}") 172 | return [] 173 | except Timeout as ex: 174 | _LOGGER.warning(f"API timeout: {ex}") 175 | return [] 176 | 177 | _LOGGER.debug(f"OK: departures for {self.stop_id}: {response.text}") 178 | 179 | # parse JSON response 180 | try: 181 | departures = response.json() 182 | except InvalidJSONError as ex: 183 | _LOGGER.error(f"API invalid JSON: {ex}") 184 | return [] 185 | 186 | if self.excluded_stops is None: 187 | excluded_stops = [] 188 | else: 189 | excluded_stops = self.excluded_stops.split(",") 190 | 191 | # convert api data into objects 192 | return [ 193 | Departure.from_dict(departure) 194 | for departure in departures.get("departures") 195 | if departure["stop"]["id"] not in excluded_stops 196 | ] 197 | 198 | def fetch_departures(self) -> list[Departure]: 199 | departures = [] 200 | 201 | if self.direction is None: 202 | departures += self.fetch_directional_departure(self.direction) 203 | else: 204 | for direction in self.direction.split(','): 205 | departures += self.fetch_directional_departure(direction) 206 | 207 | # Get rid of duplicates 208 | # Duplicates should only exist for the Ringbahn and filtering for both 209 | # directions 210 | deduplicated_departures = set(departures) 211 | 212 | return sorted(deduplicated_departures, key=lambda d: d.timestamp) 213 | 214 | def next_departure(self): 215 | if self.departures and isinstance(self.departures, list): 216 | return self.departures[0] 217 | return None 218 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold under which the program will exit with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=www 46 | 47 | # Add files or directories matching the regular expressions patterns to the 48 | # ignore-list. The regex matches against paths and can be in Posix or Windows 49 | # format. Because '\' represents the directory delimiter on Windows systems, it 50 | # can't be used as an escape character. 51 | ignore-paths= 52 | 53 | # Files or directories matching the regular expression patterns are skipped. 54 | # The regex matches against base names, not paths. The default value ignores 55 | # Emacs file locks 56 | ignore-patterns=^\.# 57 | 58 | # List of module names for which member attributes should not be checked 59 | # (useful for modules/projects where namespaces are manipulated during runtime 60 | # and thus existing member attributes cannot be deduced by static analysis). It 61 | # supports qualified module names, as well as Unix pattern matching. 62 | ignored-modules= 63 | 64 | # Python code to execute, usually for sys.path manipulation such as 65 | # pygtk.require(). 66 | #init-hook= 67 | 68 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 69 | # number of processors available to use, and will cap the count on Windows to 70 | # avoid hangs. 71 | jobs=1 72 | 73 | # Control the amount of potential inferred values when inferring a single 74 | # object. This can help the performance when dealing with large functions or 75 | # complex, nested conditions. 76 | limit-inference-results=100 77 | 78 | # List of plugins (as comma separated values of python module names) to load, 79 | # usually to register additional checkers. 80 | load-plugins= 81 | 82 | # Pickle collected data for later comparisons. 83 | persistent=yes 84 | 85 | # Minimum Python version to use for version dependent checks. Will default to 86 | # the version used to run pylint. 87 | py-version=3.10 88 | 89 | # Discover python modules and packages in the file system subtree. 90 | recursive=no 91 | 92 | # When enabled, pylint would attempt to guess common misconfiguration and emit 93 | # user-friendly hints instead of false-positive error messages. 94 | suggestion-mode=yes 95 | 96 | # Allow loading of arbitrary C extensions. Extensions are imported into the 97 | # active Python interpreter and may run arbitrary code. 98 | unsafe-load-any-extension=no 99 | 100 | # In verbose mode, extra non-checker-related info will be displayed. 101 | #verbose= 102 | 103 | 104 | [BASIC] 105 | 106 | # Naming style matching correct argument names. 107 | argument-naming-style=snake_case 108 | 109 | # Regular expression matching correct argument names. Overrides argument- 110 | # naming-style. If left empty, argument names will be checked with the set 111 | # naming style. 112 | #argument-rgx= 113 | 114 | # Naming style matching correct attribute names. 115 | attr-naming-style=snake_case 116 | 117 | # Regular expression matching correct attribute names. Overrides attr-naming- 118 | # style. If left empty, attribute names will be checked with the set naming 119 | # style. 120 | #attr-rgx= 121 | 122 | # Bad variable names which should always be refused, separated by a comma. 123 | bad-names=foo, 124 | bar, 125 | baz, 126 | toto, 127 | tutu, 128 | tata 129 | 130 | # Bad variable names regexes, separated by a comma. If names match any regex, 131 | # they will always be refused 132 | bad-names-rgxs= 133 | 134 | # Naming style matching correct class attribute names. 135 | class-attribute-naming-style=any 136 | 137 | # Regular expression matching correct class attribute names. Overrides class- 138 | # attribute-naming-style. If left empty, class attribute names will be checked 139 | # with the set naming style. 140 | #class-attribute-rgx= 141 | 142 | # Naming style matching correct class constant names. 143 | class-const-naming-style=UPPER_CASE 144 | 145 | # Regular expression matching correct class constant names. Overrides class- 146 | # const-naming-style. If left empty, class constant names will be checked with 147 | # the set naming style. 148 | #class-const-rgx= 149 | 150 | # Naming style matching correct class names. 151 | class-naming-style=PascalCase 152 | 153 | # Regular expression matching correct class names. Overrides class-naming- 154 | # style. If left empty, class names will be checked with the set naming style. 155 | #class-rgx= 156 | 157 | # Naming style matching correct constant names. 158 | const-naming-style=UPPER_CASE 159 | 160 | # Regular expression matching correct constant names. Overrides const-naming- 161 | # style. If left empty, constant names will be checked with the set naming 162 | # style. 163 | #const-rgx= 164 | 165 | # Minimum line length for functions/classes that require docstrings, shorter 166 | # ones are exempt. 167 | docstring-min-length=-1 168 | 169 | # Naming style matching correct function names. 170 | function-naming-style=snake_case 171 | 172 | # Regular expression matching correct function names. Overrides function- 173 | # naming-style. If left empty, function names will be checked with the set 174 | # naming style. 175 | #function-rgx= 176 | 177 | # Good variable names which should always be accepted, separated by a comma. 178 | good-names=i, 179 | j, 180 | k, 181 | ex, 182 | Run, 183 | _ 184 | 185 | # Good variable names regexes, separated by a comma. If names match any regex, 186 | # they will always be accepted 187 | good-names-rgxs= 188 | 189 | # Include a hint for the correct naming format with invalid-name. 190 | include-naming-hint=no 191 | 192 | # Naming style matching correct inline iteration names. 193 | inlinevar-naming-style=any 194 | 195 | # Regular expression matching correct inline iteration names. Overrides 196 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 197 | # with the set naming style. 198 | #inlinevar-rgx= 199 | 200 | # Naming style matching correct method names. 201 | method-naming-style=snake_case 202 | 203 | # Regular expression matching correct method names. Overrides method-naming- 204 | # style. If left empty, method names will be checked with the set naming style. 205 | #method-rgx= 206 | 207 | # Naming style matching correct module names. 208 | module-naming-style=snake_case 209 | 210 | # Regular expression matching correct module names. Overrides module-naming- 211 | # style. If left empty, module names will be checked with the set naming style. 212 | #module-rgx= 213 | 214 | # Colon-delimited sets of names that determine each other's naming style when 215 | # the name regexes allow several styles. 216 | name-group= 217 | 218 | # Regular expression which should only match function or class names that do 219 | # not require a docstring. 220 | no-docstring-rgx=^_ 221 | 222 | # List of decorators that produce properties, such as abc.abstractproperty. Add 223 | # to this list to register other decorators that produce valid properties. 224 | # These decorators are taken in consideration only for invalid-name. 225 | property-classes=abc.abstractproperty 226 | 227 | # Regular expression matching correct type variable names. If left empty, type 228 | # variable names will be checked with the set naming style. 229 | #typevar-rgx= 230 | 231 | # Naming style matching correct variable names. 232 | variable-naming-style=snake_case 233 | 234 | # Regular expression matching correct variable names. Overrides variable- 235 | # naming-style. If left empty, variable names will be checked with the set 236 | # naming style. 237 | #variable-rgx= 238 | 239 | 240 | [CLASSES] 241 | 242 | # Warn about protected attribute access inside special methods 243 | check-protected-access-in-special-methods=no 244 | 245 | # List of method names used to declare (i.e. assign) instance attributes. 246 | defining-attr-methods=__init__, 247 | __new__, 248 | setUp, 249 | __post_init__ 250 | 251 | # List of member names, which should be excluded from the protected access 252 | # warning. 253 | exclude-protected=_asdict, 254 | _fields, 255 | _replace, 256 | _source, 257 | _make 258 | 259 | # List of valid names for the first argument in a class method. 260 | valid-classmethod-first-arg=cls 261 | 262 | # List of valid names for the first argument in a metaclass class method. 263 | valid-metaclass-classmethod-first-arg=cls 264 | 265 | 266 | [DESIGN] 267 | 268 | # List of regular expressions of class ancestor names to ignore when counting 269 | # public methods (see R0903) 270 | exclude-too-few-public-methods= 271 | 272 | # List of qualified class names to ignore when counting class parents (see 273 | # R0901) 274 | ignored-parents= 275 | 276 | # Maximum number of arguments for function / method. 277 | max-args=5 278 | 279 | # Maximum number of attributes for a class (see R0902). 280 | max-attributes=7 281 | 282 | # Maximum number of boolean expressions in an if statement (see R0916). 283 | max-bool-expr=5 284 | 285 | # Maximum number of branch for function / method body. 286 | max-branches=12 287 | 288 | # Maximum number of locals for function / method body. 289 | max-locals=15 290 | 291 | # Maximum number of parents for a class (see R0901). 292 | max-parents=7 293 | 294 | # Maximum number of public methods for a class (see R0904). 295 | max-public-methods=20 296 | 297 | # Maximum number of return / yield for function / method body. 298 | max-returns=6 299 | 300 | # Maximum number of statements in function / method body. 301 | max-statements=50 302 | 303 | # Minimum number of public methods for a class (see R0903). 304 | min-public-methods=2 305 | 306 | 307 | [EXCEPTIONS] 308 | 309 | # Exceptions that will emit a warning when caught. 310 | overgeneral-exceptions=BaseException, 311 | Exception 312 | 313 | 314 | [FORMAT] 315 | 316 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 317 | expected-line-ending-format= 318 | 319 | # Regexp for a line that is allowed to be longer than the limit. 320 | ignore-long-lines=^\s*(# )??$ 321 | 322 | # Number of spaces of indent required inside a hanging or continued line. 323 | indent-after-paren=4 324 | 325 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 326 | # tab). 327 | indent-string=' ' 328 | 329 | # Maximum number of characters on a single line. 330 | max-line-length=100 331 | 332 | # Maximum number of lines in a module. 333 | max-module-lines=1000 334 | 335 | # Allow the body of a class to be on the same line as the declaration if body 336 | # contains single statement. 337 | single-line-class-stmt=no 338 | 339 | # Allow the body of an if to be on the same line as the test if there is no 340 | # else. 341 | single-line-if-stmt=no 342 | 343 | 344 | [IMPORTS] 345 | 346 | # List of modules that can be imported at any level, not just the top level 347 | # one. 348 | allow-any-import-level= 349 | 350 | # Allow wildcard imports from modules that define __all__. 351 | allow-wildcard-with-all=no 352 | 353 | # Deprecated modules which should not be used, separated by a comma. 354 | deprecated-modules= 355 | 356 | # Output a graph (.gv or any supported image format) of external dependencies 357 | # to the given file (report RP0402 must not be disabled). 358 | ext-import-graph= 359 | 360 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 361 | # external) dependencies to the given file (report RP0402 must not be 362 | # disabled). 363 | import-graph= 364 | 365 | # Output a graph (.gv or any supported image format) of internal dependencies 366 | # to the given file (report RP0402 must not be disabled). 367 | int-import-graph= 368 | 369 | # Force import order to recognize a module as part of the standard 370 | # compatibility libraries. 371 | known-standard-library= 372 | 373 | # Force import order to recognize a module as part of a third party library. 374 | known-third-party=enchant 375 | 376 | # Couples of modules and preferred modules, separated by a comma. 377 | preferred-modules= 378 | 379 | 380 | [LOGGING] 381 | 382 | # The type of string formatting that logging methods do. `old` means using % 383 | # formatting, `new` is for `{}` formatting. 384 | logging-format-style=old 385 | 386 | # Logging modules to check that the string format arguments are in logging 387 | # function parameter format. 388 | logging-modules=logging 389 | 390 | 391 | [MESSAGES CONTROL] 392 | 393 | # Only show warnings with the listed confidence levels. Leave empty to show 394 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 395 | # UNDEFINED. 396 | confidence=HIGH, 397 | CONTROL_FLOW, 398 | INFERENCE, 399 | INFERENCE_FAILURE, 400 | UNDEFINED 401 | 402 | # Disable the message, report, category or checker with the given id(s). You 403 | # can either give multiple identifiers separated by comma (,) or put this 404 | # option multiple times (only on the command line, not in the configuration 405 | # file where it should appear only once). You can also use "--disable=all" to 406 | # disable everything first and then re-enable specific checks. For example, if 407 | # you want to run only the similarities checker, you can use "--disable=all 408 | # --enable=similarities". If you want to run only the classes checker, but have 409 | # no Warning level messages displayed, use "--disable=all --enable=classes 410 | # --disable=W". 411 | disable=raw-checker-failed, 412 | bad-inline-option, 413 | locally-disabled, 414 | file-ignored, 415 | suppressed-message, 416 | useless-suppression, 417 | deprecated-pragma, 418 | use-symbolic-message-instead, 419 | import-error, 420 | trailing-whitespace, 421 | too-many-instance-attributes, 422 | missing-module-docstring, 423 | missing-class-docstring, 424 | missing-function-docstring, 425 | logging-fstring-interpolation, 426 | duplicate-code, 427 | 428 | # Enable the message, report, category or checker with the given id(s). You can 429 | # either give multiple identifier separated by comma (,) or put this option 430 | # multiple time (only on the command line, not in the configuration file where 431 | # it should appear only once). See also the "--disable" option for examples. 432 | enable=c-extension-no-member 433 | 434 | 435 | [METHOD_ARGS] 436 | 437 | # List of qualified names (i.e., library.method) which require a timeout 438 | # parameter e.g. 'requests.api.get,requests.api.post' 439 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 440 | 441 | 442 | [MISCELLANEOUS] 443 | 444 | # List of note tags to take in consideration, separated by a comma. 445 | notes=FIXME, 446 | XXX, 447 | TODO 448 | 449 | # Regular expression of note tags to take in consideration. 450 | notes-rgx= 451 | 452 | 453 | [REFACTORING] 454 | 455 | # Maximum number of nested blocks for function / method body 456 | max-nested-blocks=5 457 | 458 | # Complete name of functions that never returns. When checking for 459 | # inconsistent-return-statements if a never returning function is called then 460 | # it will be considered as an explicit return statement and no message will be 461 | # printed. 462 | never-returning-functions=sys.exit,argparse.parse_error 463 | 464 | 465 | [REPORTS] 466 | 467 | # Python expression which should return a score less than or equal to 10. You 468 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 469 | # 'convention', and 'info' which contain the number of messages in each 470 | # category, as well as 'statement' which is the total number of statements 471 | # analyzed. This score is used by the global evaluation report (RP0004). 472 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 473 | 474 | # Template used to display messages. This is a python new-style format string 475 | # used to format the message information. See doc for all details. 476 | msg-template= 477 | 478 | # Set the output format. Available formats are text, parseable, colorized, json 479 | # and msvs (visual studio). You can also give a reporter class, e.g. 480 | # mypackage.mymodule.MyReporterClass. 481 | #output-format= 482 | 483 | # Tells whether to display a full report or only the messages. 484 | reports=no 485 | 486 | # Activate the evaluation score. 487 | score=yes 488 | 489 | 490 | [SIMILARITIES] 491 | 492 | # Comments are removed from the similarity computation 493 | ignore-comments=yes 494 | 495 | # Docstrings are removed from the similarity computation 496 | ignore-docstrings=yes 497 | 498 | # Imports are removed from the similarity computation 499 | ignore-imports=yes 500 | 501 | # Signatures are removed from the similarity computation 502 | ignore-signatures=yes 503 | 504 | # Minimum lines number of a similarity. 505 | min-similarity-lines=4 506 | 507 | 508 | [SPELLING] 509 | 510 | # Limits count of emitted suggestions for spelling mistakes. 511 | max-spelling-suggestions=4 512 | 513 | # Spelling dictionary name. Available dictionaries: none. To make it work, 514 | # install the 'python-enchant' package. 515 | spelling-dict= 516 | 517 | # List of comma separated words that should be considered directives if they 518 | # appear at the beginning of a comment and should not be checked. 519 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 520 | 521 | # List of comma separated words that should not be checked. 522 | spelling-ignore-words= 523 | 524 | # A path to a file that contains the private dictionary; one word per line. 525 | spelling-private-dict-file= 526 | 527 | # Tells whether to store unknown words to the private dictionary (see the 528 | # --spelling-private-dict-file option) instead of raising a message. 529 | spelling-store-unknown-words=no 530 | 531 | 532 | [STRING] 533 | 534 | # This flag controls whether inconsistent-quotes generates a warning when the 535 | # character used as a quote delimiter is used inconsistently within a module. 536 | check-quote-consistency=no 537 | 538 | # This flag controls whether the implicit-str-concat should generate a warning 539 | # on implicit string concatenation in sequences defined over several lines. 540 | check-str-concat-over-line-jumps=no 541 | 542 | 543 | [TYPECHECK] 544 | 545 | # List of decorators that produce context managers, such as 546 | # contextlib.contextmanager. Add to this list to register other decorators that 547 | # produce valid context managers. 548 | contextmanager-decorators=contextlib.contextmanager 549 | 550 | # List of members which are set dynamically and missed by pylint inference 551 | # system, and so shouldn't trigger E1101 when accessed. Python regular 552 | # expressions are accepted. 553 | generated-members= 554 | 555 | # Tells whether to warn about missing members when the owner of the attribute 556 | # is inferred to be None. 557 | ignore-none=yes 558 | 559 | # This flag controls whether pylint should warn about no-member and similar 560 | # checks whenever an opaque object is returned when inferring. The inference 561 | # can return multiple potential results while evaluating a Python object, but 562 | # some branches might not be evaluated, which results in partial inference. In 563 | # that case, it might be useful to still emit no-member and other checks for 564 | # the rest of the inferred objects. 565 | ignore-on-opaque-inference=yes 566 | 567 | # List of symbolic message names to ignore for Mixin members. 568 | ignored-checks-for-mixins=no-member, 569 | not-async-context-manager, 570 | not-context-manager, 571 | attribute-defined-outside-init 572 | 573 | # List of class names for which member attributes should not be checked (useful 574 | # for classes with dynamically set attributes). This supports the use of 575 | # qualified names. 576 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 577 | 578 | # Show a hint with possible names when a member name was not found. The aspect 579 | # of finding the hint is based on edit distance. 580 | missing-member-hint=yes 581 | 582 | # The minimum edit distance a name should have in order to be considered a 583 | # similar match for a missing member name. 584 | missing-member-hint-distance=1 585 | 586 | # The total number of similar names that should be taken in consideration when 587 | # showing a hint for a missing member. 588 | missing-member-max-choices=1 589 | 590 | # Regex pattern to define which classes are considered mixins. 591 | mixin-class-rgx=.*[Mm]ixin 592 | 593 | # List of decorators that change the signature of a decorated function. 594 | signature-mutators= 595 | 596 | 597 | [VARIABLES] 598 | 599 | # List of additional names supposed to be defined in builtins. Remember that 600 | # you should avoid defining new builtins when possible. 601 | additional-builtins= 602 | 603 | # Tells whether unused global variables should be treated as a violation. 604 | allow-global-unused-variables=yes 605 | 606 | # List of names allowed to shadow builtins 607 | allowed-redefined-builtins= 608 | 609 | # List of strings which can identify a callback function by name. A callback 610 | # name must start or end with one of those strings. 611 | callbacks=cb_, 612 | _cb 613 | 614 | # A regular expression matching the name of dummy variables (i.e. expected to 615 | # not be used). 616 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 617 | 618 | # Argument names that match this expression will be ignored. 619 | ignored-argument-names=_.*|^ignored_|^unused_ 620 | 621 | # Tells whether we should check for unused import in __init__ files. 622 | init-import=no 623 | 624 | # List of qualified module names which can have objects that can redefine 625 | # builtins. 626 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 627 | --------------------------------------------------------------------------------