├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── doc └── full.png ├── docker-compose.yaml ├── pyproject.toml ├── requirements.txt ├── saves └── partial_saved_data_2025_04_06.txt ├── setup.cfg ├── src ├── __init__.py └── dataimporter │ ├── __init__.py │ ├── dashboard_utils.py │ ├── importer.py │ └── message_handler.py ├── storage ├── grafana │ ├── dashboards │ │ └── dashboard.json │ ├── data │ │ └── .gitignore │ └── provisioning │ │ ├── dashboards │ │ └── default.yaml │ │ └── datasources │ │ └── datasource.yaml └── influxdb │ └── .gitignore └── test └── dataimportertest ├── __init__.py └── test_handle_message.py /.dockerignore: -------------------------------------------------------------------------------- 1 | storage/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | storage/grafana/data 2 | storage/influxdb 3 | unused.txt 4 | /experimental 5 | /cache 6 | 7 | /venv 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | pip-wheel-metadata/ 32 | share/python-wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | MANIFEST 37 | 38 | # PyInstaller 39 | # Usually these files are written by a python script from a template 40 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 41 | *.manifest 42 | *.spec 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # Environments 66 | .env 67 | .venv 68 | env/ 69 | venv/ 70 | ENV/ 71 | env.bak/ 72 | venv.bak/ 73 | 74 | .idea 75 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13 2 | ENV TZ=Europe/Berlin 3 | 4 | RUN mkdir /app 5 | WORKDIR /app 6 | 7 | COPY setup.cfg /app/setup.cfg 8 | COPY pyproject.toml /app/pyproject.toml 9 | COPY src/ /app/src 10 | RUN pip install . 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # f1-live-data 2 | You want a better view of the live data of a F1 race? f1-live-data is easy to use and customizable to your needs. 3 | 4 | ## Requirements 5 | - Docker installed 6 | - python 3.13 (for developing) 7 | 8 | ## Quick start 9 | Tested on Ubuntu 22.04 10 | ``` 11 | chmod -R 777 storage/ 12 | docker compose up -d 13 | docker build -t data-importer-image . 14 | 15 | # if a f1 race is currently under way: 16 | docker run -it --rm \ 17 | --network f1-live-data_default \ 18 | data-importer-image \ 19 | dataimporter process-live-session \ 20 | --influx-url http://influxdb:8086 21 | 22 | # else 23 | docker run -it --rm \ 24 | --network f1-live-data_default \ 25 | -v ${PWD}/saves/partial_saved_data_2025_04_06.txt:/tmp/save.txt \ 26 | data-importer-image \ 27 | dataimporter process-mock-data /tmp/save.txt \ 28 | --influx-url http://influxdb:8086 29 | 30 | # Browse http://localhost:3000 31 | # admin / admin 32 | # Dashboards > Browse > F1 > F1Race 33 | ``` 34 | 35 | ## Run the data-importer locally (for debugging) 36 | ``` 37 | docker compose up -d 38 | pip install . 39 | dataimporter process-mock-data saves/partial_saved_data_2025_04_06.txt --influx-url http://localhost:8086 40 | ``` 41 | 42 | ## Features 43 | ![](doc/full.png) 44 | - Select only your favorite drivers (top, left) 45 | - Leaderboard with Interval, Gap to Leader, Last Lap Time 46 | - Lap time evolution 47 | - Race control messages 48 | - Top speed at speed trap 49 | - Gap to leader graph 50 | - Weather data 51 | 52 | 53 | ## Data flow 54 | ``` 55 | ┌─────────────┐ ┌────────┐ ┌───────┐ 56 | │data-importer├─────►│influxdb│◄─────┤grafana│ 57 | └─────────────┘ └────────┘ └───────┘ 58 | ``` 59 | The `data-importer` uses the live timing client from `fastf1` to receive live timing data during a f1 session. 60 | The data is stored in an `influxdb`. `grafana` is used to display the data by querying it from `influxdb`. 61 | 62 | The `data-importer` has two modes: 63 | - process-live-session: Processes data from a live session via `fastf1` live timing client. 64 | - process-mock-data: Loads data from file and replays it (with a default speedup factor of 100). This mode can be used to develop new panels and debug it. 65 | 66 | ## Processed data 67 | `fastf1` provided a bunch of different data points. Not all of them are processed: 68 | ``` 69 | Processed: WeatherData, RaceControlMessages, TimingData 70 | Not Processed: Heartbeat,CarData.z,Position.z,ExtrapolatedClock,TopThree,RcmSeries,TimingStats,TimingAppData,TrackStatus,DriverList,SessionInfo,SessionData,LapCount 71 | ``` 72 | 73 | 74 | ## Get a file with live data via fastf1 python package 75 | You can record a live session with the live timing client from `fastf1` 76 | ``` 77 | python -m fastf1.livetiming save saved_data_2022_03_19.txt 78 | ``` 79 | The recorded file can be used to develop and test the data processing. 80 | The data-importer is able to load the recorded file (command `process-mock-data`) 81 | 82 | ## Tricks 83 | 84 | ### Add driver color to same panels 85 | To add the color of a driver to a panel via the UI can be annoying. 86 | There is a command line tool do accomplish that. 87 | Just pass the path of the dashboard and the names of the panels: 88 | ```shell 89 | python src/dataimporter/dashboard_utils.py storage/grafana/dashboards/dashboard.json "Lap time" "Gap To Leader" 90 | ``` 91 | 92 | ### Edit and persist a grafana dashboard/panel: 93 | 1. Edit the panel in the UI 94 | 2. Click the save button 95 | 3. Click "Copy JSON to clipboard" 96 | 4. Replace the content of the file `storage/grafana/dashboards/dashboard.json` 97 | 98 | ### Set a max value in lap time panel 99 | Laps with pit stops are very slow and lead to a large value range on the y axes. 100 | You can set a maximum value in the panel settings. 101 | 102 | ## Known issues 103 | - data-importer disconnects after 2h (the f1 data providing service seems to close the connection, see [fastf1](https://theoehrly.github.io/Fast-F1/livetiming.html?highlight=live#important-notes)) 104 | 105 | ## Further ideas 106 | - Display car position (`Position.z`) 107 | - Display telemetry (`CarData.z`) 108 | - Display (personal) fastest lap 109 | - Display sector times 110 | - Number pit stops and tires compound (`TimingAppData`) 111 | -------------------------------------------------------------------------------- /doc/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f1stuff/f1-live-data/ed018f2e2bd217d2eb968bcc71c5c02cd4288033/doc/full.png -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | grafana: 4 | image: grafana/grafana:11.6.0 5 | container_name: grafana 6 | #restart: always 7 | ports: 8 | - '3000:3000' 9 | volumes: 10 | - './storage/grafana/data:/var/lib/grafana' 11 | - './storage/grafana/provisioning:/etc/grafana/provisioning/' 12 | - './storage/grafana/dashboards:/tmp/dashboards/' 13 | 14 | influxdb: 15 | image: influxdb:2.7.11-alpine 16 | container_name: influxdb 17 | #restart: always 18 | ports: 19 | - '8086:8086' 20 | environment: 21 | - DOCKER_INFLUXDB_INIT_MODE=setup 22 | - DOCKER_INFLUXDB_INIT_USERNAME=admin 23 | - DOCKER_INFLUXDB_INIT_PASSWORD=password 24 | - DOCKER_INFLUXDB_INIT_ORG=f1 25 | - DOCKER_INFLUXDB_INIT_BUCKET=data 26 | - DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=LoOFvHw1tUXrUZ8oUqaozmEjxxG9UNO5H5YfRI4cGu306xwQVu_KMNxRYRMrWbhdD886N2PuRgpo9v4v_58pHw== 27 | volumes: 28 | - './storage/influxdb:/var/lib/influxdb2' 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastf1~=3.5.3 2 | influxdb-client~=1.48.0 3 | typer~=0.15.2 4 | jq~=1.8.0 5 | pytest==8.3.5 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = dataimporter 3 | version = 1.0.0 4 | author = f1guy 5 | description = Python package to import f1 live data to influxdb 6 | keywords = python, import influxdb 7 | 8 | [options] 9 | packages = find: 10 | package_dir = 11 | = src 12 | python_requires= >3.7, <3.14 13 | install_requires = 14 | fastf1~=3.5.3 15 | influxdb-client~=1.48.0 16 | typer~=0.15.2 17 | 18 | [options.packages.find] 19 | where = src 20 | include = * 21 | exclude = 22 | 23 | [options.entry_points] 24 | console_scripts = 25 | dataimporter = dataimporter.importer:main 26 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f1stuff/f1-live-data/ed018f2e2bd217d2eb968bcc71c5c02cd4288033/src/__init__.py -------------------------------------------------------------------------------- /src/dataimporter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f1stuff/f1-live-data/ed018f2e2bd217d2eb968bcc71c5c02cd4288033/src/dataimporter/__init__.py -------------------------------------------------------------------------------- /src/dataimporter/dashboard_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import List 3 | 4 | import typer 5 | 6 | from dataimporter.message_handler import D_LOOKUP 7 | 8 | app = typer.Typer() 9 | 10 | 11 | @app.command() 12 | def add_driver_color_to_dashboard_overrides(path_to_dashboard_json: str, dashboard_titles_to_process: List[str]): 13 | driver_color_list_overrides = [] 14 | for driver_info in D_LOOKUP: 15 | driver_color_list_overrides.append( 16 | _color_matcher_section(driver_info[1], driver_info[3], driver_info[4] == 'DOT')) 17 | print(json.dumps(driver_color_list_overrides)) 18 | 19 | driver_names = [driver[1] for driver in D_LOOKUP] 20 | print(driver_names) 21 | 22 | with open(path_to_dashboard_json, "r") as dashboard: 23 | dashboard_contents = dashboard.read() 24 | parsed_json = json.loads(dashboard_contents) 25 | 26 | # print(parsed_json) 27 | i = 0 28 | for panel in parsed_json['panels']: 29 | if panel['title'] in dashboard_titles_to_process and 'fieldConfig' in panel and 'overrides' in panel[ 30 | 'fieldConfig']: 31 | print("==================", panel['title'], "==================") 32 | overrides_without_driver_color = [o for o in panel['fieldConfig']['overrides'] 33 | if 'matcher' not in o 34 | or 'id' not in o['matcher'] 35 | or o['matcher']['id'] != 'byName' 36 | or 'options' not in o['matcher'] 37 | or o['matcher']['options'] not in driver_names 38 | ] 39 | 40 | print("================== overrides_without_driver_color ==================") 41 | print(overrides_without_driver_color) 42 | 43 | print("================== new overrides_without_driver_color ==================") 44 | overrides_without_driver_color.extend(driver_color_list_overrides) 45 | print(overrides_without_driver_color) 46 | parsed_json['panels'][i]['fieldConfig']['overrides'] = overrides_without_driver_color 47 | i += 1 48 | 49 | # Writing to sample.json 50 | with open(path_to_dashboard_json, "w") as dashboard: 51 | dashboard.write(json.dumps(parsed_json, indent=4)) 52 | 53 | 54 | def _color_matcher_section(driver_name, driver_color_hex, dotted_line=False): 55 | if dotted_line: 56 | return { 57 | "matcher": { 58 | "id": "byName", 59 | "options": driver_name 60 | }, 61 | "properties": [ 62 | { 63 | "id": "color", 64 | "value": { 65 | "fixedColor": driver_color_hex, 66 | "mode": "fixed" 67 | } 68 | }, { 69 | "id": "custom.lineStyle", 70 | "value": { 71 | "dash": [ 72 | 0, 73 | 5 74 | ], 75 | "fill": "dot" 76 | } 77 | } 78 | ] 79 | } 80 | else: 81 | return { 82 | "matcher": { 83 | "id": "byName", 84 | "options": driver_name 85 | }, 86 | "properties": [ 87 | { 88 | "id": "color", 89 | "value": { 90 | "fixedColor": driver_color_hex, 91 | "mode": "fixed" 92 | } 93 | } 94 | ] 95 | } 96 | 97 | 98 | def main(): 99 | app() 100 | 101 | 102 | if __name__ == '__main__': 103 | main() 104 | -------------------------------------------------------------------------------- /src/dataimporter/importer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from functools import partial 3 | import json 4 | import time 5 | import types 6 | import logging 7 | 8 | import typer 9 | from dataimporter.message_handler import handle_message 10 | from fastf1.utils import to_datetime 11 | from influxdb_client import InfluxDBClient 12 | from influxdb_client.client.write_api import SYNCHRONOUS 13 | from fastf1.livetiming.client import SignalRClient 14 | 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | format="%(asctime)s - %(levelname)s - %(message)s", # Log format 18 | handlers=[ 19 | logging.StreamHandler(), 20 | ], 21 | ) 22 | log = logging.getLogger(__name__) 23 | 24 | token = "LoOFvHw1tUXrUZ8oUqaozmEjxxG9UNO5H5YfRI4cGu306xwQVu_KMNxRYRMrWbhdD886N2PuRgpo9v4v_58pHw==" 25 | org = "f1" 26 | bucket = "data" 27 | 28 | app = typer.Typer() 29 | 30 | 31 | def fix_json(line): 32 | # fix F1's not json compliant data 33 | line = line.replace("'", '"') \ 34 | .replace('True', 'true') \ 35 | .replace('False', 'false') 36 | return line 37 | 38 | 39 | def _to_file_overwrite(write_api, self, msg): 40 | msg = fix_json(msg) 41 | try: 42 | cat, msg, dt = json.loads(msg) 43 | except (json.JSONDecodeError, ValueError): 44 | log.warning("Json parse error") 45 | return 46 | 47 | # convert string to datetime 48 | try: 49 | dt = to_datetime(dt) 50 | except (ValueError, TypeError): 51 | log.warning("Datetime parse error {}", dt) 52 | return 53 | 54 | points = handle_message(cat, msg, dt) 55 | for point in points: 56 | write_api.write(bucket, org, point) 57 | 58 | 59 | def _start_overwrite(self): 60 | """Connect to the data stream and start writing the data.""" 61 | try: 62 | asyncio.run(self._async_start()) 63 | except KeyboardInterrupt: 64 | self.logger.warning("Keyboard interrupt - exiting...") 65 | raise KeyboardInterrupt 66 | 67 | 68 | def store_live_data(write_api): 69 | """ 70 | Stores live data in influx database 71 | This function only works if a f1 live session is active. 72 | """ 73 | try: 74 | retries = 0 75 | while retries < 5: 76 | retries = retries + 1 77 | client = SignalRClient("unused.txt") 78 | client.topics = ["Heartbeat", "WeatherData", "RaceControlMessages", "TimingData"] 79 | overwrite = partial(_to_file_overwrite, write_api) 80 | client._to_file = types.MethodType(overwrite, 81 | client) # Override _to_file methode from fastf1 so the data will be stored in db rather than in a file 82 | client.start = types.MethodType(_start_overwrite, client) 83 | client.start() 84 | except KeyboardInterrupt: 85 | print('interrupted!') 86 | 87 | 88 | def store_mock_data(write_api, path_to_saved_data, speedup_factor=100): 89 | # Load old live data from file 90 | with open(path_to_saved_data) as file: 91 | lines = [line.rstrip() for line in file] 92 | 93 | msg = fix_json(lines[0]) 94 | cat, msg, dt = json.loads(msg) 95 | dt_last_record = to_datetime(dt) 96 | 97 | for line in lines: 98 | msg = fix_json(line) 99 | cat, msg, dt = json.loads(msg) 100 | dt = to_datetime(dt) 101 | 102 | waiting_time = max(0.0, (dt - dt_last_record).total_seconds() / speedup_factor) 103 | log.debug("Waiting time: %s", waiting_time) 104 | dt_last_record = dt 105 | time.sleep(waiting_time) 106 | 107 | dt_now = int(time.time() * 1000000000) 108 | points = handle_message(cat, msg, dt_now) 109 | for point in points: 110 | write_api.write(bucket, org, point) 111 | 112 | 113 | @app.command() 114 | def process_live_session(influx_url="http://localhost:8086"): 115 | with InfluxDBClient(url=influx_url, token=token, org=org) as client: 116 | write_api = client.write_api(write_options=SYNCHRONOUS) 117 | store_live_data(write_api) 118 | return 0 119 | 120 | 121 | @app.command() 122 | def process_mock_data(path_to_saved_data, influx_url="http://localhost:8086", speedup_factor: int = 100): 123 | with InfluxDBClient(url=influx_url, token=token, org=org) as client: 124 | write_api = client.write_api(write_options=SYNCHRONOUS) 125 | store_mock_data(write_api, path_to_saved_data, speedup_factor) 126 | return 0 127 | 128 | 129 | def main(): 130 | app() 131 | 132 | 133 | if __name__ == '__main__': 134 | main() 135 | -------------------------------------------------------------------------------- /src/dataimporter/message_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from influxdb_client import Point, WritePrecision 3 | 4 | logging.basicConfig( 5 | level=logging.INFO, 6 | format="%(asctime)s - %(levelname)s - %(message)s", # Log format 7 | handlers=[ 8 | logging.StreamHandler(), 9 | ], 10 | ) 11 | log = logging.getLogger(__name__) 12 | 13 | D_LOOKUP = [[12, 'ANT', 'Mercedes', '#27F4D2', 'DOT'], [63, 'RUS', 'Mercedes', '#27F4D2', 'SOLID'], 14 | [44, 'HAM', 'Ferrari', '#E8002D', 'DOT'], [16, 'LEC', 'Ferrari', '#E8002D', 'SOLID'], 15 | [1, 'VER', 'Red Bull Racing', '#3671C6', 'SOLID'], [22, 'TSU', 'Red Bull Racing', '#3671C6', 'DOT'], 16 | [81, 'PIA', 'McLaren', '#FF8000', 'DOT'], [4, 'NOR', 'McLaren', '#FF8000', 'SOLID'], 17 | [14, 'ALO', 'Aston Martin', '#229971', 'SOLID'], [18, 'STR', 'Aston Martin', '#229971', 'DOT'], 18 | [10, 'GAS', 'Alpine', '#00A1E8', 'SOLID'], [43, 'COL', 'Alpine', '#00A1E8', 'DOT'], 19 | [30, 'LAW', 'Racing Bulls', '#6692FF', 'SOLID'], [6, 'HAD', 'Racing Bulls', '#6692FF', 'DOT'], 20 | [31, 'OCO', 'Haas', '#B6BABD', 'SOLID'], [87, 'BEA', 'Haas', '#B6BABD', 'DOT'], 21 | [27, 'HUL', 'Kick Sauber', '#52E252', 'SOLID'], [5, 'BOR', 'Kick Sauber', '#52E252', 'DOT'], 22 | [55, 'SAI', 'Williams', '#1868DB', 'DOT'], [23, 'ALB', 'Williams', '#1868DB', 'SOLID']] 23 | 24 | 25 | def driver_no_to_name(driver_no: str) -> str: 26 | for entry in D_LOOKUP: 27 | if str(entry[0]) == driver_no: 28 | return entry[1] 29 | log.warning("Driver not found %s", driver_no) 30 | return "UKN" 31 | 32 | 33 | def get_LastLapTime_from_TimingData(message): 34 | # message example: {'Lines': {'77': {'Sectors': {'2': {'PreviousValue': '34.125'}}, 'LastLapTime': {'Value': '1:30.997', 'PersonalFastest': False}}}} 35 | results = [] 36 | items = message["Lines"].items() 37 | for driverNo, value in items: 38 | if "LastLapTime" in value: 39 | if "Value" in value["LastLapTime"]: 40 | result = driverNo, lap_time_to_timespan(value["LastLapTime"]["Value"]) 41 | results.append(result) 42 | return results 43 | 44 | 45 | def get_GapToLeader_from_TimingData(message): 46 | # message example: {'Lines': {'11': {'GapToLeader': '+41.005', 'IntervalToPositionAhead': {'Value': '+28.876'}}}} 47 | results = [] 48 | items = message["Lines"].items() 49 | for driverNo, value in items: 50 | if "GapToLeader" in value and value["GapToLeader"] != "": 51 | result = driverNo, value["GapToLeader"] 52 | results.append(result) 53 | return results 54 | 55 | 56 | def get_NumberOfLaps_from_TimingData(message): 57 | # message example: {'Lines': {'10': {'NumberOfLaps': 7, 'Sectors': {'2': {'Value': '25.040'}}, 'Speeds': {'FL': {'Value': '285'}}, 'LastLapTime': {'Value': '1:41.134'}}}} 58 | results = [] 59 | items = message["Lines"].items() 60 | for driverNo, value in items: 61 | if "NumberOfLaps" in value and value["NumberOfLaps"] != "": 62 | result = driverNo, value["NumberOfLaps"] 63 | results.append(result) 64 | return results 65 | 66 | 67 | def get_IntervalToPositionAhead_from_TimingData(message): 68 | # message example: {'Lines': {'11': {'GapToLeader': '+41.005', 'IntervalToPositionAhead': {'Value': '+28.876'}}}} 69 | results = [] 70 | items = message["Lines"].items() 71 | for driverNo, value in items: 72 | if "IntervalToPositionAhead" in value: 73 | if "Value" in value["IntervalToPositionAhead"] and value["IntervalToPositionAhead"]["Value"] != "": 74 | result = driverNo, interval_to_timespan(value["IntervalToPositionAhead"]["Value"]) 75 | results.append(result) 76 | return results 77 | 78 | 79 | def get_SpeedTrap_from_TimingData(message): 80 | # message example: {'Lines': {'55': {'Speeds': {'ST': {'Value': '294'}}}}} 81 | # ST = Speed Trap, I1 = Intermediate 1, Intermeditate 2, FL = Finish Line 82 | results = [] 83 | items = message["Lines"].items() 84 | for driverNo, value in items: 85 | if "Speeds" in value: 86 | speed_values = list(value['Speeds'].items()) 87 | for position, speed in speed_values: 88 | if position == 'ST' and 'Value' in speed and speed['Value'] != "": 89 | results.append((driverNo, int(speed['Value']))) 90 | return results 91 | 92 | 93 | def lap_time_to_timespan(time_string) -> float: 94 | m, remain = time_string.split(':') 95 | s, ms = remain.split(".") 96 | return int(m) * 60 + int(s) + int(ms) / 1000 97 | 98 | 99 | def interval_to_timespan(time_string) -> float: 100 | if "LAP" in time_string: 101 | return 0.0 102 | if " L" in time_string: # Lapped cars have "1 L", "2 L", ... gap to leader 103 | return 500.0 # TODO how to handle lapped cars? 104 | s, ms = time_string.replace("+", "").split('.') 105 | return int(s) + int(ms) / 1000 106 | 107 | 108 | def interval_human_readable(time_string) -> float: 109 | if time_string[0] == "+": 110 | return time_string[1:] 111 | return time_string 112 | 113 | 114 | def extract_TimingData(value, datetime): 115 | points = [] 116 | try: 117 | # Last lap time 118 | result = get_LastLapTime_from_TimingData(value) 119 | if len(result) > 0: 120 | for r in result: 121 | driverNo, speed = r 122 | driverName = driver_no_to_name(driverNo) 123 | log.info("Last lap: %s - %s", driverName, speed) 124 | 125 | point = Point("lastLapTime") \ 126 | .field("LastLapTime", speed) \ 127 | .tag("driver", driverName) \ 128 | .time(datetime, WritePrecision.NS) 129 | points.append(point) 130 | 131 | # GapToLeader 132 | result = get_GapToLeader_from_TimingData(value) 133 | if len(result) > 0: 134 | for r in result: 135 | driverNo, gap = r 136 | driverName = driver_no_to_name(driverNo) 137 | log.info("Gap to Leader: %s - %s", driverName, gap) 138 | 139 | point = Point("gapToLeader") \ 140 | .field("GapToLeader", interval_to_timespan(gap)) \ 141 | .field("GapToLeaderHumanReadable", interval_human_readable(gap)) \ 142 | .tag("driver", driverName) \ 143 | .time(datetime, WritePrecision.NS) 144 | points.append(point) 145 | 146 | # IntervalToPositionAhead 147 | result = get_IntervalToPositionAhead_from_TimingData(value) 148 | if len(result) > 0: 149 | for r in result: 150 | driverNo, interval = r 151 | driverName = driver_no_to_name(driverNo) 152 | log.info("Gap to Leader: %s - %s", driverName, interval) 153 | 154 | point = Point("intervalToPositionAhead") \ 155 | .field("IntervalToPositionAhead", interval) \ 156 | .tag("driver", driverName) \ 157 | .time(datetime, WritePrecision.NS) 158 | points.append(point) 159 | 160 | # NumberOfLaps 161 | result = get_NumberOfLaps_from_TimingData(value) 162 | if len(result) > 0: 163 | for r in result: 164 | driverNo, lapNumber = r 165 | driverName = driver_no_to_name(driverNo) 166 | log.info("NumberOfLaps: %s - %s", driverName, lapNumber) 167 | 168 | point = Point("numberOfLaps") \ 169 | .field("NumberOfLaps", lapNumber) \ 170 | .tag("driver", driverName) \ 171 | .time(datetime, WritePrecision.NS) 172 | points.append(point) 173 | 174 | # Speed trap 175 | result = get_SpeedTrap_from_TimingData(value) 176 | if len(result) > 0: 177 | for r in result: 178 | driverNo, speed = r 179 | driverName = driver_no_to_name(driverNo) 180 | log.info("Speed trap: %s - %s", driverName, speed) 181 | 182 | point = Point("speedTrap") \ 183 | .field("Speed", speed) \ 184 | .tag("driver", driverName) \ 185 | .time(datetime, WritePrecision.NS) 186 | points.append(point) 187 | except Exception as e: 188 | log.exception("Unable to extract TimingData: %s %s", value, e) 189 | 190 | return points 191 | 192 | 193 | def extract_RaceControlMessages(value, datetime): 194 | try: 195 | rcm = list(value["Messages"].values())[0]["Message"] 196 | log.info("RaceControlMessage: %s", rcm) 197 | 198 | point = Point("raceControlMessage") \ 199 | .field("Message", rcm) \ 200 | .time(datetime, WritePrecision.NS) 201 | return [point] 202 | except Exception as e: 203 | log.exception("Unable to extract RaceControlMessages: %s %s", value, e) 204 | return [] 205 | 206 | 207 | def extract_WeatherData(value, datetime): 208 | try: 209 | w = value 210 | log.info("WeatherData: %s", w) 211 | 212 | point = Point("weatherData") \ 213 | .field("AirTemp", float(w["AirTemp"])) \ 214 | .field("Humidity", float(w["Humidity"])) \ 215 | .field("Pressure", float(w["Pressure"])) \ 216 | .field("Rainfall", float(w["Rainfall"])) \ 217 | .field("TrackTemp", float(w["TrackTemp"])) \ 218 | .field("WindDirection", float(w["WindDirection"])) \ 219 | .field("WindSpeed", float(w["WindSpeed"])) \ 220 | .time(datetime, WritePrecision.NS) 221 | return [point] 222 | except Exception as e: 223 | log.exception("Unable to extract WeatherData: %s %s", value, e) 224 | return [] 225 | 226 | 227 | def handle_message(key, message, datetime) -> list: 228 | if key == 'TimingData': 229 | return extract_TimingData(message, datetime) 230 | elif key == 'RaceControlMessages': 231 | return extract_RaceControlMessages(message, datetime) 232 | elif key == 'WeatherData': 233 | return extract_WeatherData(message, datetime) 234 | else: 235 | return [] 236 | -------------------------------------------------------------------------------- /storage/grafana/dashboards/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "datasource", 8 | "uid": "grafana" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "target": { 15 | "limit": 100, 16 | "matchAny": false, 17 | "tags": [], 18 | "type": "dashboard" 19 | }, 20 | "type": "dashboard" 21 | } 22 | ] 23 | }, 24 | "editable": true, 25 | "fiscalYearStartMonth": 0, 26 | "graphTooltip": 0, 27 | "links": [], 28 | "liveNow": false, 29 | "panels": [ 30 | { 31 | "datasource": { 32 | "type": "influxdb", 33 | "uid": "P951FEA4DE68E13C5" 34 | }, 35 | "description": "", 36 | "fieldConfig": { 37 | "defaults": { 38 | "color": { 39 | "mode": "thresholds" 40 | }, 41 | "custom": { 42 | "align": "auto", 43 | "cellOptions": { 44 | "type": "color-text" 45 | }, 46 | "filterable": false, 47 | "inspect": false 48 | }, 49 | "mappings": [], 50 | "thresholds": { 51 | "mode": "absolute", 52 | "steps": [ 53 | { 54 | "color": "text", 55 | "value": null 56 | }, 57 | { 58 | "color": "red", 59 | "value": 0.001 60 | }, 61 | { 62 | "color": "super-light-yellow", 63 | "value": 1000 64 | }, 65 | { 66 | "color": "text", 67 | "value": 2000 68 | } 69 | ] 70 | }, 71 | "unit": "string" 72 | }, 73 | "overrides": [ 74 | { 75 | "matcher": { 76 | "id": "byName", 77 | "options": "driver" 78 | }, 79 | "properties": [ 80 | { 81 | "id": "custom.width", 82 | "value": 61 83 | }, 84 | { 85 | "id": "color", 86 | "value": { 87 | "fixedColor": "text", 88 | "mode": "fixed" 89 | } 90 | } 91 | ] 92 | }, 93 | { 94 | "matcher": { 95 | "id": "byName", 96 | "options": "Interval" 97 | }, 98 | "properties": [ 99 | { 100 | "id": "custom.width", 101 | "value": 69 102 | }, 103 | { 104 | "id": "unit", 105 | "value": "time: s.SSS" 106 | } 107 | ] 108 | }, 109 | { 110 | "matcher": { 111 | "id": "byName", 112 | "options": " " 113 | }, 114 | "properties": [ 115 | { 116 | "id": "custom.width", 117 | "value": 45 118 | }, 119 | { 120 | "id": "unit", 121 | "value": "none" 122 | }, 123 | { 124 | "id": "thresholds", 125 | "value": { 126 | "mode": "absolute", 127 | "steps": [ 128 | { 129 | "color": "text", 130 | "value": null 131 | } 132 | ] 133 | } 134 | } 135 | ] 136 | }, 137 | { 138 | "matcher": { 139 | "id": "byName", 140 | "options": "Last Lap" 141 | }, 142 | "properties": [ 143 | { 144 | "id": "custom.width", 145 | "value": 74 146 | }, 147 | { 148 | "id": "unit", 149 | "value": "time: m:ss:SSS" 150 | } 151 | ] 152 | }, 153 | { 154 | "matcher": { 155 | "id": "byName", 156 | "options": "Gap Leader" 157 | }, 158 | "properties": [ 159 | { 160 | "id": "custom.width", 161 | "value": 96 162 | } 163 | ] 164 | }, 165 | { 166 | "matcher": { 167 | "id": "byName", 168 | "options": "_value_gap" 169 | }, 170 | "properties": [ 171 | { 172 | "id": "custom.width", 173 | "value": 93 174 | } 175 | ] 176 | } 177 | ] 178 | }, 179 | "gridPos": { 180 | "h": 22, 181 | "w": 5, 182 | "x": 0, 183 | "y": 0 184 | }, 185 | "id": 13, 186 | "options": { 187 | "footer": { 188 | "countRows": false, 189 | "fields": "", 190 | "reducer": [ 191 | "sum" 192 | ], 193 | "show": false 194 | }, 195 | "frameIndex": 5, 196 | "showHeader": true, 197 | "sortBy": [] 198 | }, 199 | "pluginVersion": "9.4.3", 200 | "targets": [ 201 | { 202 | "datasource": { 203 | "type": "influxdb", 204 | "uid": "P951FEA4DE68E13C5" 205 | }, 206 | "query": "gapDataHumanReadable = from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"gapToLeader\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"GapToLeaderHumanReadable\")\r\n |> last()\r\n |> group()\r\n |> keep(columns: [\"driver\", \"_value\"])\r\n\r\ngapData = from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"gapToLeader\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"GapToLeader\")\r\n |> last()\r\n |> group()\r\n |> keep(columns: [\"driver\", \"_value\"])\r\n \r\nintervalData = from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"intervalToPositionAhead\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"IntervalToPositionAhead\")\r\n |> last()\r\n |> group()\r\n |> keep(columns: [\"driver\", \"_value\"])\r\n\r\nlastLapData = from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"lastLapTime\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"LastLapTime\")\r\n |> last()\r\n |> group()\r\n |> keep(columns: [\"driver\", \"_value\"])\r\n\r\nnumberLapsData = from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"numberOfLaps\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"NumberOfLaps\")\r\n |> last()\r\n |> group()\r\n |> keep(columns: [\"driver\", \"_value\", \"_time\"]) \r\n\r\nintermediateJoinData1 = join(\r\n tables: {gapHumanReadable:gapDataHumanReadable, interval:intervalData},\r\n on: [\"driver\"],\r\n)\r\n\r\n \r\nintermediateJoinData2 = join(\r\n tables: {intermediateJoin1:intermediateJoinData1, numberLaps:numberLapsData},\r\n on: [\"driver\"],\r\n)\r\n \r\nintermediateJoinData3 = join(\r\n tables: {intermediateJoin1:intermediateJoinData2, gap:gapData},\r\n on: [\"driver\"],\r\n)\r\n\r\njoin(\r\n tables: {intermediateJoin:intermediateJoinData3, lastLap:lastLapData},\r\n on: [\"driver\"],\r\n) |> sort(columns: [\"_time\"], desc: false) \r\n |> sort(columns: [\"_value_gap\"], desc: false)\r\n |> sort(columns: [\"_value_intermediateJoin1\"], desc: true)\r\n |> fill(column: \"position\", value: 1.0)\r\n |> cumulativeSum(columns: [\"position\"])\r\n |> map(fn: (r) => ({ r with interval: r._value_interval * 1000.0 }))\r\n |> map(fn: (r) => ({ r with lastLap: r._value * 1000.0 }))", 207 | "refId": "A" 208 | } 209 | ], 210 | "title": "Standing", 211 | "transformations": [ 212 | { 213 | "id": "organize", 214 | "options": { 215 | "excludeByName": { 216 | "_time": true, 217 | "_value": true, 218 | "_value_gap": true, 219 | "_value_intermediateJoin": false, 220 | "_value_intermediateJoin1": true, 221 | "_value_interval": true, 222 | "_value_lastLap": false, 223 | "lastLap": false 224 | }, 225 | "indexByName": { 226 | "_time": 6, 227 | "_value_gapHumanReadable": 4, 228 | "_value_intermediateJoin": 7, 229 | "_value_interval": 2, 230 | "_value_lastLap": 8, 231 | "driver": 1, 232 | "interval": 3, 233 | "lastLap": 5, 234 | "position": 0 235 | }, 236 | "renameByName": { 237 | "_time": "", 238 | "_value": "", 239 | "_value_gap": "", 240 | "_value_gapHumanReadable": "Gap Leader", 241 | "_value_interval": "", 242 | "_value_lastLap": "", 243 | "driver": " ", 244 | "gap": "Gap Leader", 245 | "interval": "Interval", 246 | "lastLap": "Last Lap", 247 | "position": " " 248 | } 249 | } 250 | }, 251 | { 252 | "id": "sortBy", 253 | "options": { 254 | "fields": {}, 255 | "sort": [ 256 | { 257 | "field": "Gap To Leader" 258 | } 259 | ] 260 | } 261 | } 262 | ], 263 | "type": "table" 264 | }, 265 | { 266 | "datasource": { 267 | "type": "influxdb", 268 | "uid": "P951FEA4DE68E13C5" 269 | }, 270 | "description": "", 271 | "fieldConfig": { 272 | "defaults": { 273 | "color": { 274 | "mode": "palette-classic" 275 | }, 276 | "custom": { 277 | "axisCenteredZero": false, 278 | "axisColorMode": "text", 279 | "axisLabel": "", 280 | "axisPlacement": "auto", 281 | "barAlignment": 0, 282 | "drawStyle": "line", 283 | "fillOpacity": 0, 284 | "gradientMode": "none", 285 | "hideFrom": { 286 | "legend": false, 287 | "tooltip": false, 288 | "viz": false 289 | }, 290 | "lineInterpolation": "linear", 291 | "lineWidth": 2, 292 | "pointSize": 5, 293 | "scaleDistribution": { 294 | "type": "linear" 295 | }, 296 | "showPoints": "always", 297 | "spanNulls": false, 298 | "stacking": { 299 | "group": "A", 300 | "mode": "none" 301 | }, 302 | "thresholdsStyle": { 303 | "mode": "off" 304 | } 305 | }, 306 | "displayName": "${__field.labels.driver}", 307 | "mappings": [], 308 | "max": 101000, 309 | "thresholds": { 310 | "mode": "absolute", 311 | "steps": [ 312 | { 313 | "color": "green", 314 | "value": null 315 | } 316 | ] 317 | }, 318 | "unit": "time: m:ss:SSS" 319 | }, 320 | "overrides": [ 321 | { 322 | "matcher": { 323 | "id": "byName", 324 | "options": "ANT" 325 | }, 326 | "properties": [ 327 | { 328 | "id": "color", 329 | "value": { 330 | "fixedColor": "#27F4D2", 331 | "mode": "fixed" 332 | } 333 | }, 334 | { 335 | "id": "custom.lineStyle", 336 | "value": { 337 | "dash": [ 338 | 0, 339 | 5 340 | ], 341 | "fill": "dot" 342 | } 343 | } 344 | ] 345 | }, 346 | { 347 | "matcher": { 348 | "id": "byName", 349 | "options": "RUS" 350 | }, 351 | "properties": [ 352 | { 353 | "id": "color", 354 | "value": { 355 | "fixedColor": "#27F4D2", 356 | "mode": "fixed" 357 | } 358 | } 359 | ] 360 | }, 361 | { 362 | "matcher": { 363 | "id": "byName", 364 | "options": "HAM" 365 | }, 366 | "properties": [ 367 | { 368 | "id": "color", 369 | "value": { 370 | "fixedColor": "#E8002D", 371 | "mode": "fixed" 372 | } 373 | }, 374 | { 375 | "id": "custom.lineStyle", 376 | "value": { 377 | "dash": [ 378 | 0, 379 | 5 380 | ], 381 | "fill": "dot" 382 | } 383 | } 384 | ] 385 | }, 386 | { 387 | "matcher": { 388 | "id": "byName", 389 | "options": "LEC" 390 | }, 391 | "properties": [ 392 | { 393 | "id": "color", 394 | "value": { 395 | "fixedColor": "#E8002D", 396 | "mode": "fixed" 397 | } 398 | } 399 | ] 400 | }, 401 | { 402 | "matcher": { 403 | "id": "byName", 404 | "options": "VER" 405 | }, 406 | "properties": [ 407 | { 408 | "id": "color", 409 | "value": { 410 | "fixedColor": "#3671C6", 411 | "mode": "fixed" 412 | } 413 | } 414 | ] 415 | }, 416 | { 417 | "matcher": { 418 | "id": "byName", 419 | "options": "TSU" 420 | }, 421 | "properties": [ 422 | { 423 | "id": "color", 424 | "value": { 425 | "fixedColor": "#3671C6", 426 | "mode": "fixed" 427 | } 428 | }, 429 | { 430 | "id": "custom.lineStyle", 431 | "value": { 432 | "dash": [ 433 | 0, 434 | 5 435 | ], 436 | "fill": "dot" 437 | } 438 | } 439 | ] 440 | }, 441 | { 442 | "matcher": { 443 | "id": "byName", 444 | "options": "PIA" 445 | }, 446 | "properties": [ 447 | { 448 | "id": "color", 449 | "value": { 450 | "fixedColor": "#FF8000", 451 | "mode": "fixed" 452 | } 453 | }, 454 | { 455 | "id": "custom.lineStyle", 456 | "value": { 457 | "dash": [ 458 | 0, 459 | 5 460 | ], 461 | "fill": "dot" 462 | } 463 | } 464 | ] 465 | }, 466 | { 467 | "matcher": { 468 | "id": "byName", 469 | "options": "NOR" 470 | }, 471 | "properties": [ 472 | { 473 | "id": "color", 474 | "value": { 475 | "fixedColor": "#FF8000", 476 | "mode": "fixed" 477 | } 478 | } 479 | ] 480 | }, 481 | { 482 | "matcher": { 483 | "id": "byName", 484 | "options": "ALO" 485 | }, 486 | "properties": [ 487 | { 488 | "id": "color", 489 | "value": { 490 | "fixedColor": "#229971", 491 | "mode": "fixed" 492 | } 493 | } 494 | ] 495 | }, 496 | { 497 | "matcher": { 498 | "id": "byName", 499 | "options": "STR" 500 | }, 501 | "properties": [ 502 | { 503 | "id": "color", 504 | "value": { 505 | "fixedColor": "#229971", 506 | "mode": "fixed" 507 | } 508 | }, 509 | { 510 | "id": "custom.lineStyle", 511 | "value": { 512 | "dash": [ 513 | 0, 514 | 5 515 | ], 516 | "fill": "dot" 517 | } 518 | } 519 | ] 520 | }, 521 | { 522 | "matcher": { 523 | "id": "byName", 524 | "options": "GAS" 525 | }, 526 | "properties": [ 527 | { 528 | "id": "color", 529 | "value": { 530 | "fixedColor": "#00A1E8", 531 | "mode": "fixed" 532 | } 533 | } 534 | ] 535 | }, 536 | { 537 | "matcher": { 538 | "id": "byName", 539 | "options": "COL" 540 | }, 541 | "properties": [ 542 | { 543 | "id": "color", 544 | "value": { 545 | "fixedColor": "#00A1E8", 546 | "mode": "fixed" 547 | } 548 | }, 549 | { 550 | "id": "custom.lineStyle", 551 | "value": { 552 | "dash": [ 553 | 0, 554 | 5 555 | ], 556 | "fill": "dot" 557 | } 558 | } 559 | ] 560 | }, 561 | { 562 | "matcher": { 563 | "id": "byName", 564 | "options": "LAW" 565 | }, 566 | "properties": [ 567 | { 568 | "id": "color", 569 | "value": { 570 | "fixedColor": "#6692FF", 571 | "mode": "fixed" 572 | } 573 | } 574 | ] 575 | }, 576 | { 577 | "matcher": { 578 | "id": "byName", 579 | "options": "HAD" 580 | }, 581 | "properties": [ 582 | { 583 | "id": "color", 584 | "value": { 585 | "fixedColor": "#6692FF", 586 | "mode": "fixed" 587 | } 588 | }, 589 | { 590 | "id": "custom.lineStyle", 591 | "value": { 592 | "dash": [ 593 | 0, 594 | 5 595 | ], 596 | "fill": "dot" 597 | } 598 | } 599 | ] 600 | }, 601 | { 602 | "matcher": { 603 | "id": "byName", 604 | "options": "OCO" 605 | }, 606 | "properties": [ 607 | { 608 | "id": "color", 609 | "value": { 610 | "fixedColor": "#B6BABD", 611 | "mode": "fixed" 612 | } 613 | } 614 | ] 615 | }, 616 | { 617 | "matcher": { 618 | "id": "byName", 619 | "options": "BEA" 620 | }, 621 | "properties": [ 622 | { 623 | "id": "color", 624 | "value": { 625 | "fixedColor": "#B6BABD", 626 | "mode": "fixed" 627 | } 628 | }, 629 | { 630 | "id": "custom.lineStyle", 631 | "value": { 632 | "dash": [ 633 | 0, 634 | 5 635 | ], 636 | "fill": "dot" 637 | } 638 | } 639 | ] 640 | }, 641 | { 642 | "matcher": { 643 | "id": "byName", 644 | "options": "HUL" 645 | }, 646 | "properties": [ 647 | { 648 | "id": "color", 649 | "value": { 650 | "fixedColor": "#52E252", 651 | "mode": "fixed" 652 | } 653 | } 654 | ] 655 | }, 656 | { 657 | "matcher": { 658 | "id": "byName", 659 | "options": "BOR" 660 | }, 661 | "properties": [ 662 | { 663 | "id": "color", 664 | "value": { 665 | "fixedColor": "#52E252", 666 | "mode": "fixed" 667 | } 668 | }, 669 | { 670 | "id": "custom.lineStyle", 671 | "value": { 672 | "dash": [ 673 | 0, 674 | 5 675 | ], 676 | "fill": "dot" 677 | } 678 | } 679 | ] 680 | }, 681 | { 682 | "matcher": { 683 | "id": "byName", 684 | "options": "SAI" 685 | }, 686 | "properties": [ 687 | { 688 | "id": "color", 689 | "value": { 690 | "fixedColor": "#1868DB", 691 | "mode": "fixed" 692 | } 693 | }, 694 | { 695 | "id": "custom.lineStyle", 696 | "value": { 697 | "dash": [ 698 | 0, 699 | 5 700 | ], 701 | "fill": "dot" 702 | } 703 | } 704 | ] 705 | }, 706 | { 707 | "matcher": { 708 | "id": "byName", 709 | "options": "ALB" 710 | }, 711 | "properties": [ 712 | { 713 | "id": "color", 714 | "value": { 715 | "fixedColor": "#1868DB", 716 | "mode": "fixed" 717 | } 718 | } 719 | ] 720 | } 721 | ] 722 | }, 723 | "gridPos": { 724 | "h": 9, 725 | "w": 19, 726 | "x": 5, 727 | "y": 0 728 | }, 729 | "id": 8, 730 | "options": { 731 | "legend": { 732 | "calcs": [], 733 | "displayMode": "list", 734 | "placement": "right", 735 | "showLegend": true 736 | }, 737 | "tooltip": { 738 | "mode": "single", 739 | "sort": "none" 740 | } 741 | }, 742 | "pluginVersion": "9.4.3", 743 | "targets": [ 744 | { 745 | "datasource": { 746 | "type": "influxdb", 747 | "uid": "P951FEA4DE68E13C5" 748 | }, 749 | "query": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"lastLapTime\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"LastLapTime\")\r\n |> filter(fn: (r) => r[\"driver\"] =~ /${Drivers:regex}/)\r\n |> map(fn: (r) => ({r with lastLap: r._value * 1000.0}))\r\n |> keep(columns: [\"driver\", \"lastLap\", \"_time\"])\r\n ", 750 | "refId": "A" 751 | } 752 | ], 753 | "title": "Lap time", 754 | "transformations": [], 755 | "type": "timeseries" 756 | }, 757 | { 758 | "datasource": { 759 | "type": "influxdb", 760 | "uid": "P951FEA4DE68E13C5" 761 | }, 762 | "description": "", 763 | "gridPos": { 764 | "h": 7, 765 | "w": 11, 766 | "x": 5, 767 | "y": 9 768 | }, 769 | "id": 4, 770 | "options": { 771 | "dedupStrategy": "none", 772 | "enableLogDetails": true, 773 | "prettifyLogMessage": false, 774 | "showCommonLabels": false, 775 | "showLabels": false, 776 | "showTime": false, 777 | "sortOrder": "Descending", 778 | "wrapLogMessage": true 779 | }, 780 | "targets": [ 781 | { 782 | "datasource": { 783 | "type": "influxdb", 784 | "uid": "P951FEA4DE68E13C5" 785 | }, 786 | "query": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"raceControlMessage\")", 787 | "refId": "A" 788 | } 789 | ], 790 | "title": "Race control messages", 791 | "type": "logs" 792 | }, 793 | { 794 | "datasource": { 795 | "type": "influxdb", 796 | "uid": "P951FEA4DE68E13C5" 797 | }, 798 | "description": "", 799 | "fieldConfig": { 800 | "defaults": { 801 | "color": { 802 | "mode": "thresholds" 803 | }, 804 | "displayName": "${__field.labels.driver}", 805 | "mappings": [], 806 | "thresholds": { 807 | "mode": "absolute", 808 | "steps": [ 809 | { 810 | "color": "green", 811 | "value": null 812 | }, 813 | { 814 | "color": "#EAB839", 815 | "value": 250 816 | }, 817 | { 818 | "color": "semi-dark-red", 819 | "value": 300 820 | } 821 | ] 822 | }, 823 | "unit": "velocitykmh" 824 | }, 825 | "overrides": [] 826 | }, 827 | "gridPos": { 828 | "h": 7, 829 | "w": 8, 830 | "x": 16, 831 | "y": 9 832 | }, 833 | "id": 10, 834 | "options": { 835 | "displayMode": "lcd", 836 | "minVizHeight": 10, 837 | "minVizWidth": 0, 838 | "orientation": "auto", 839 | "reduceOptions": { 840 | "calcs": [ 841 | "lastNotNull" 842 | ], 843 | "fields": "", 844 | "values": false 845 | }, 846 | "showUnfilled": true, 847 | "text": {} 848 | }, 849 | "pluginVersion": "9.4.3", 850 | "targets": [ 851 | { 852 | "datasource": { 853 | "type": "influxdb", 854 | "uid": "P951FEA4DE68E13C5" 855 | }, 856 | "query": "from(bucket: \"data\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"speedTrap\")\n |> filter(fn: (r) => r[\"driver\"] =~ /${Drivers:regex}/)", 857 | "refId": "A" 858 | } 859 | ], 860 | "title": "Last top speed (speed trap)", 861 | "type": "bargauge" 862 | }, 863 | { 864 | "datasource": { 865 | "type": "influxdb", 866 | "uid": "P951FEA4DE68E13C5" 867 | }, 868 | "description": "", 869 | "fieldConfig": { 870 | "defaults": { 871 | "color": { 872 | "mode": "palette-classic" 873 | }, 874 | "custom": { 875 | "axisCenteredZero": false, 876 | "axisColorMode": "text", 877 | "axisLabel": "", 878 | "axisPlacement": "auto", 879 | "barAlignment": 0, 880 | "drawStyle": "line", 881 | "fillOpacity": 0, 882 | "gradientMode": "none", 883 | "hideFrom": { 884 | "legend": false, 885 | "tooltip": false, 886 | "viz": false 887 | }, 888 | "lineInterpolation": "linear", 889 | "lineWidth": 2, 890 | "pointSize": 5, 891 | "scaleDistribution": { 892 | "type": "linear" 893 | }, 894 | "showPoints": "auto", 895 | "spanNulls": false, 896 | "stacking": { 897 | "group": "A", 898 | "mode": "none" 899 | }, 900 | "thresholdsStyle": { 901 | "mode": "off" 902 | } 903 | }, 904 | "displayName": "${__field.labels.driver}", 905 | "mappings": [], 906 | "min": -2, 907 | "thresholds": { 908 | "mode": "absolute", 909 | "steps": [ 910 | { 911 | "color": "green", 912 | "value": null 913 | } 914 | ] 915 | }, 916 | "unit": "clocks" 917 | }, 918 | "overrides": [ 919 | { 920 | "matcher": { 921 | "id": "byName", 922 | "options": "ANT" 923 | }, 924 | "properties": [ 925 | { 926 | "id": "color", 927 | "value": { 928 | "fixedColor": "#27F4D2", 929 | "mode": "fixed" 930 | } 931 | }, 932 | { 933 | "id": "custom.lineStyle", 934 | "value": { 935 | "dash": [ 936 | 0, 937 | 5 938 | ], 939 | "fill": "dot" 940 | } 941 | } 942 | ] 943 | }, 944 | { 945 | "matcher": { 946 | "id": "byName", 947 | "options": "RUS" 948 | }, 949 | "properties": [ 950 | { 951 | "id": "color", 952 | "value": { 953 | "fixedColor": "#27F4D2", 954 | "mode": "fixed" 955 | } 956 | } 957 | ] 958 | }, 959 | { 960 | "matcher": { 961 | "id": "byName", 962 | "options": "HAM" 963 | }, 964 | "properties": [ 965 | { 966 | "id": "color", 967 | "value": { 968 | "fixedColor": "#E8002D", 969 | "mode": "fixed" 970 | } 971 | }, 972 | { 973 | "id": "custom.lineStyle", 974 | "value": { 975 | "dash": [ 976 | 0, 977 | 5 978 | ], 979 | "fill": "dot" 980 | } 981 | } 982 | ] 983 | }, 984 | { 985 | "matcher": { 986 | "id": "byName", 987 | "options": "LEC" 988 | }, 989 | "properties": [ 990 | { 991 | "id": "color", 992 | "value": { 993 | "fixedColor": "#E8002D", 994 | "mode": "fixed" 995 | } 996 | } 997 | ] 998 | }, 999 | { 1000 | "matcher": { 1001 | "id": "byName", 1002 | "options": "VER" 1003 | }, 1004 | "properties": [ 1005 | { 1006 | "id": "color", 1007 | "value": { 1008 | "fixedColor": "#3671C6", 1009 | "mode": "fixed" 1010 | } 1011 | } 1012 | ] 1013 | }, 1014 | { 1015 | "matcher": { 1016 | "id": "byName", 1017 | "options": "TSU" 1018 | }, 1019 | "properties": [ 1020 | { 1021 | "id": "color", 1022 | "value": { 1023 | "fixedColor": "#3671C6", 1024 | "mode": "fixed" 1025 | } 1026 | }, 1027 | { 1028 | "id": "custom.lineStyle", 1029 | "value": { 1030 | "dash": [ 1031 | 0, 1032 | 5 1033 | ], 1034 | "fill": "dot" 1035 | } 1036 | } 1037 | ] 1038 | }, 1039 | { 1040 | "matcher": { 1041 | "id": "byName", 1042 | "options": "PIA" 1043 | }, 1044 | "properties": [ 1045 | { 1046 | "id": "color", 1047 | "value": { 1048 | "fixedColor": "#FF8000", 1049 | "mode": "fixed" 1050 | } 1051 | }, 1052 | { 1053 | "id": "custom.lineStyle", 1054 | "value": { 1055 | "dash": [ 1056 | 0, 1057 | 5 1058 | ], 1059 | "fill": "dot" 1060 | } 1061 | } 1062 | ] 1063 | }, 1064 | { 1065 | "matcher": { 1066 | "id": "byName", 1067 | "options": "NOR" 1068 | }, 1069 | "properties": [ 1070 | { 1071 | "id": "color", 1072 | "value": { 1073 | "fixedColor": "#FF8000", 1074 | "mode": "fixed" 1075 | } 1076 | } 1077 | ] 1078 | }, 1079 | { 1080 | "matcher": { 1081 | "id": "byName", 1082 | "options": "ALO" 1083 | }, 1084 | "properties": [ 1085 | { 1086 | "id": "color", 1087 | "value": { 1088 | "fixedColor": "#229971", 1089 | "mode": "fixed" 1090 | } 1091 | } 1092 | ] 1093 | }, 1094 | { 1095 | "matcher": { 1096 | "id": "byName", 1097 | "options": "STR" 1098 | }, 1099 | "properties": [ 1100 | { 1101 | "id": "color", 1102 | "value": { 1103 | "fixedColor": "#229971", 1104 | "mode": "fixed" 1105 | } 1106 | }, 1107 | { 1108 | "id": "custom.lineStyle", 1109 | "value": { 1110 | "dash": [ 1111 | 0, 1112 | 5 1113 | ], 1114 | "fill": "dot" 1115 | } 1116 | } 1117 | ] 1118 | }, 1119 | { 1120 | "matcher": { 1121 | "id": "byName", 1122 | "options": "GAS" 1123 | }, 1124 | "properties": [ 1125 | { 1126 | "id": "color", 1127 | "value": { 1128 | "fixedColor": "#00A1E8", 1129 | "mode": "fixed" 1130 | } 1131 | } 1132 | ] 1133 | }, 1134 | { 1135 | "matcher": { 1136 | "id": "byName", 1137 | "options": "COL" 1138 | }, 1139 | "properties": [ 1140 | { 1141 | "id": "color", 1142 | "value": { 1143 | "fixedColor": "#00A1E8", 1144 | "mode": "fixed" 1145 | } 1146 | }, 1147 | { 1148 | "id": "custom.lineStyle", 1149 | "value": { 1150 | "dash": [ 1151 | 0, 1152 | 5 1153 | ], 1154 | "fill": "dot" 1155 | } 1156 | } 1157 | ] 1158 | }, 1159 | { 1160 | "matcher": { 1161 | "id": "byName", 1162 | "options": "LAW" 1163 | }, 1164 | "properties": [ 1165 | { 1166 | "id": "color", 1167 | "value": { 1168 | "fixedColor": "#6692FF", 1169 | "mode": "fixed" 1170 | } 1171 | } 1172 | ] 1173 | }, 1174 | { 1175 | "matcher": { 1176 | "id": "byName", 1177 | "options": "HAD" 1178 | }, 1179 | "properties": [ 1180 | { 1181 | "id": "color", 1182 | "value": { 1183 | "fixedColor": "#6692FF", 1184 | "mode": "fixed" 1185 | } 1186 | }, 1187 | { 1188 | "id": "custom.lineStyle", 1189 | "value": { 1190 | "dash": [ 1191 | 0, 1192 | 5 1193 | ], 1194 | "fill": "dot" 1195 | } 1196 | } 1197 | ] 1198 | }, 1199 | { 1200 | "matcher": { 1201 | "id": "byName", 1202 | "options": "OCO" 1203 | }, 1204 | "properties": [ 1205 | { 1206 | "id": "color", 1207 | "value": { 1208 | "fixedColor": "#B6BABD", 1209 | "mode": "fixed" 1210 | } 1211 | } 1212 | ] 1213 | }, 1214 | { 1215 | "matcher": { 1216 | "id": "byName", 1217 | "options": "BEA" 1218 | }, 1219 | "properties": [ 1220 | { 1221 | "id": "color", 1222 | "value": { 1223 | "fixedColor": "#B6BABD", 1224 | "mode": "fixed" 1225 | } 1226 | }, 1227 | { 1228 | "id": "custom.lineStyle", 1229 | "value": { 1230 | "dash": [ 1231 | 0, 1232 | 5 1233 | ], 1234 | "fill": "dot" 1235 | } 1236 | } 1237 | ] 1238 | }, 1239 | { 1240 | "matcher": { 1241 | "id": "byName", 1242 | "options": "HUL" 1243 | }, 1244 | "properties": [ 1245 | { 1246 | "id": "color", 1247 | "value": { 1248 | "fixedColor": "#52E252", 1249 | "mode": "fixed" 1250 | } 1251 | } 1252 | ] 1253 | }, 1254 | { 1255 | "matcher": { 1256 | "id": "byName", 1257 | "options": "BOR" 1258 | }, 1259 | "properties": [ 1260 | { 1261 | "id": "color", 1262 | "value": { 1263 | "fixedColor": "#52E252", 1264 | "mode": "fixed" 1265 | } 1266 | }, 1267 | { 1268 | "id": "custom.lineStyle", 1269 | "value": { 1270 | "dash": [ 1271 | 0, 1272 | 5 1273 | ], 1274 | "fill": "dot" 1275 | } 1276 | } 1277 | ] 1278 | }, 1279 | { 1280 | "matcher": { 1281 | "id": "byName", 1282 | "options": "SAI" 1283 | }, 1284 | "properties": [ 1285 | { 1286 | "id": "color", 1287 | "value": { 1288 | "fixedColor": "#1868DB", 1289 | "mode": "fixed" 1290 | } 1291 | }, 1292 | { 1293 | "id": "custom.lineStyle", 1294 | "value": { 1295 | "dash": [ 1296 | 0, 1297 | 5 1298 | ], 1299 | "fill": "dot" 1300 | } 1301 | } 1302 | ] 1303 | }, 1304 | { 1305 | "matcher": { 1306 | "id": "byName", 1307 | "options": "ALB" 1308 | }, 1309 | "properties": [ 1310 | { 1311 | "id": "color", 1312 | "value": { 1313 | "fixedColor": "#1868DB", 1314 | "mode": "fixed" 1315 | } 1316 | } 1317 | ] 1318 | } 1319 | ] 1320 | }, 1321 | "gridPos": { 1322 | "h": 9, 1323 | "w": 19, 1324 | "x": 5, 1325 | "y": 16 1326 | }, 1327 | "id": 14, 1328 | "options": { 1329 | "legend": { 1330 | "calcs": [], 1331 | "displayMode": "list", 1332 | "placement": "right", 1333 | "showLegend": true 1334 | }, 1335 | "tooltip": { 1336 | "mode": "single", 1337 | "sort": "none" 1338 | } 1339 | }, 1340 | "pluginVersion": "8.2.5", 1341 | "targets": [ 1342 | { 1343 | "datasource": { 1344 | "type": "influxdb", 1345 | "uid": "P951FEA4DE68E13C5" 1346 | }, 1347 | "query": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"gapToLeader\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"GapToLeader\")\r\n |> filter(fn: (r) => r[\"driver\"] =~ /${Drivers:regex}/)", 1348 | "refId": "A" 1349 | } 1350 | ], 1351 | "title": "Gap To Leader", 1352 | "transformations": [], 1353 | "type": "timeseries" 1354 | }, 1355 | { 1356 | "datasource": { 1357 | "type": "influxdb", 1358 | "uid": "P951FEA4DE68E13C5" 1359 | }, 1360 | "description": "", 1361 | "fieldConfig": { 1362 | "defaults": { 1363 | "color": { 1364 | "mode": "palette-classic" 1365 | }, 1366 | "custom": { 1367 | "axisCenteredZero": false, 1368 | "axisColorMode": "text", 1369 | "axisLabel": "", 1370 | "axisPlacement": "auto", 1371 | "barAlignment": 0, 1372 | "drawStyle": "line", 1373 | "fillOpacity": 0, 1374 | "gradientMode": "none", 1375 | "hideFrom": { 1376 | "legend": false, 1377 | "tooltip": false, 1378 | "viz": false 1379 | }, 1380 | "lineInterpolation": "linear", 1381 | "lineWidth": 2, 1382 | "pointSize": 5, 1383 | "scaleDistribution": { 1384 | "type": "linear" 1385 | }, 1386 | "showPoints": "auto", 1387 | "spanNulls": false, 1388 | "stacking": { 1389 | "group": "A", 1390 | "mode": "none" 1391 | }, 1392 | "thresholdsStyle": { 1393 | "mode": "off" 1394 | } 1395 | }, 1396 | "mappings": [], 1397 | "thresholds": { 1398 | "mode": "absolute", 1399 | "steps": [ 1400 | { 1401 | "color": "green", 1402 | "value": null 1403 | }, 1404 | { 1405 | "color": "red", 1406 | "value": 80 1407 | } 1408 | ] 1409 | } 1410 | }, 1411 | "overrides": [ 1412 | { 1413 | "matcher": { 1414 | "id": "byName", 1415 | "options": "TrackTemp" 1416 | }, 1417 | "properties": [ 1418 | { 1419 | "id": "color", 1420 | "value": { 1421 | "fixedColor": "semi-dark-red", 1422 | "mode": "fixed" 1423 | } 1424 | } 1425 | ] 1426 | }, 1427 | { 1428 | "matcher": { 1429 | "id": "byName", 1430 | "options": "AirTemp" 1431 | }, 1432 | "properties": [ 1433 | { 1434 | "id": "color", 1435 | "value": { 1436 | "fixedColor": "semi-dark-blue", 1437 | "mode": "fixed" 1438 | } 1439 | } 1440 | ] 1441 | }, 1442 | { 1443 | "matcher": { 1444 | "id": "byName", 1445 | "options": "Rainfall" 1446 | }, 1447 | "properties": [ 1448 | { 1449 | "id": "color", 1450 | "value": { 1451 | "fixedColor": "dark-green", 1452 | "mode": "fixed" 1453 | } 1454 | } 1455 | ] 1456 | }, 1457 | { 1458 | "matcher": { 1459 | "id": "byName", 1460 | "options": "WindSpeed" 1461 | }, 1462 | "properties": [ 1463 | { 1464 | "id": "color", 1465 | "value": { 1466 | "fixedColor": "semi-dark-yellow", 1467 | "mode": "fixed" 1468 | } 1469 | } 1470 | ] 1471 | }, 1472 | { 1473 | "__systemRef": "hideSeriesFrom", 1474 | "matcher": { 1475 | "id": "byNames", 1476 | "options": { 1477 | "mode": "exclude", 1478 | "names": [ 1479 | "AirTemp", 1480 | "TrackTemp" 1481 | ], 1482 | "prefix": "All except:", 1483 | "readOnly": true 1484 | } 1485 | }, 1486 | "properties": [ 1487 | { 1488 | "id": "custom.hideFrom", 1489 | "value": { 1490 | "legend": false, 1491 | "tooltip": false, 1492 | "viz": true 1493 | } 1494 | } 1495 | ] 1496 | } 1497 | ] 1498 | }, 1499 | "gridPos": { 1500 | "h": 6, 1501 | "w": 5, 1502 | "x": 0, 1503 | "y": 22 1504 | }, 1505 | "id": 6, 1506 | "options": { 1507 | "legend": { 1508 | "calcs": [ 1509 | "lastNotNull" 1510 | ], 1511 | "displayMode": "list", 1512 | "placement": "right", 1513 | "showLegend": true 1514 | }, 1515 | "tooltip": { 1516 | "mode": "single", 1517 | "sort": "none" 1518 | } 1519 | }, 1520 | "targets": [ 1521 | { 1522 | "datasource": { 1523 | "type": "influxdb", 1524 | "uid": "P951FEA4DE68E13C5" 1525 | }, 1526 | "query": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"weatherData\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"AirTemp\" or r[\"_field\"] == \"TrackTemp\" or r[\"_field\"] == \"WindSpeed\" or r[\"_field\"] == \"Rainfall\")", 1527 | "refId": "A" 1528 | } 1529 | ], 1530 | "title": "Weather data", 1531 | "transformations": [], 1532 | "type": "timeseries" 1533 | } 1534 | ], 1535 | "refresh": false, 1536 | "revision": 1, 1537 | "schemaVersion": 38, 1538 | "style": "dark", 1539 | "tags": [], 1540 | "templating": { 1541 | "list": [ 1542 | { 1543 | "current": { 1544 | "selected": true, 1545 | "text": [ 1546 | "ALO", 1547 | "LEC", 1548 | "VER" 1549 | ], 1550 | "value": [ 1551 | "ALO", 1552 | "LEC", 1553 | "VER" 1554 | ] 1555 | }, 1556 | "datasource": { 1557 | "type": "influxdb", 1558 | "uid": "P951FEA4DE68E13C5" 1559 | }, 1560 | "definition": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"lastLapTime\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"LastLapTime\")\r\n |> group(columns: [\"driver\"])\r\n |> distinct(column: \"driver\")\r\n |> keep(columns: [\"_value\"])", 1561 | "description": "All drivers", 1562 | "hide": 1, 1563 | "includeAll": true, 1564 | "label": "driver", 1565 | "multi": true, 1566 | "name": "Drivers", 1567 | "options": [], 1568 | "query": "from(bucket: \"data\")\r\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\r\n |> filter(fn: (r) => r[\"_measurement\"] == \"lastLapTime\")\r\n |> filter(fn: (r) => r[\"_field\"] == \"LastLapTime\")\r\n |> group(columns: [\"driver\"])\r\n |> distinct(column: \"driver\")\r\n |> keep(columns: [\"_value\"])", 1569 | "refresh": 1, 1570 | "regex": "", 1571 | "skipUrlSync": false, 1572 | "sort": 0, 1573 | "type": "query" 1574 | } 1575 | ] 1576 | }, 1577 | "time": { 1578 | "from": "2023-03-17T21:39:50.000Z", 1579 | "to": "2023-03-17T21:47:02.000Z" 1580 | }, 1581 | "timepicker": { 1582 | "refresh_intervals": [ 1583 | "5s", 1584 | "10s", 1585 | "30s", 1586 | "1m" 1587 | ] 1588 | }, 1589 | "timezone": "", 1590 | "title": "F1Race", 1591 | "uid": "3fbFhK-4k", 1592 | "version": 1, 1593 | "weekStart": "" 1594 | } -------------------------------------------------------------------------------- /storage/grafana/data/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /storage/grafana/provisioning/dashboards/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: DefaultProvider 5 | orgId: 1 6 | folder: F1 7 | type: file 8 | options: 9 | path: /tmp/dashboards/ 10 | -------------------------------------------------------------------------------- /storage/grafana/provisioning/datasources/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: InfluxDB 5 | type: influxdb 6 | access: proxy 7 | uid: P951FEA4DE68E13C5 8 | orgId: 1 9 | url: http://influxdb:8086 10 | user: 11 | database: 12 | basicAuth: false 13 | basicAuthUser: 14 | isDefault: true 15 | jsonData: 16 | defaultBucket: data 17 | httpMode: POST 18 | organization: f1 19 | version: Flux 20 | readOnly: true 21 | secureJsonData: 22 | token: LoOFvHw1tUXrUZ8oUqaozmEjxxG9UNO5H5YfRI4cGu306xwQVu_KMNxRYRMrWbhdD886N2PuRgpo9v4v_58pHw== 23 | version: 1 24 | editable: true 25 | -------------------------------------------------------------------------------- /storage/influxdb/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /test/dataimportertest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f1stuff/f1-live-data/ed018f2e2bd217d2eb968bcc71c5c02cd4288033/test/dataimportertest/__init__.py -------------------------------------------------------------------------------- /test/dataimportertest/test_handle_message.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from fastf1.utils import to_datetime 4 | 5 | from dataimporter.importer import fix_json 6 | from dataimporter.message_handler import handle_message 7 | 8 | 9 | def test_gap_to_leader_and_interval_to_pos_ahead(): 10 | msg = "['TimingData', {'Lines': {'63': {'GapToLeader': '+44.163', 'IntervalToPositionAhead': {'Value': '+1.667'}}}}, '2023-03-05T16:08:38.509Z']" 11 | msg = fix_json(msg) 12 | cat, msg, dt = json.loads(msg) 13 | dt = to_datetime(dt) 14 | points = handle_message(cat, msg, dt) 15 | assert len(points) == 2 16 | assert points[0]._name == "gapToLeader" 17 | assert points[1]._name == "intervalToPositionAhead" 18 | 19 | assert "driver" in points[0]._tags 20 | assert points[0]._tags["driver"] == "RUS" 21 | 22 | assert "GapToLeader" in points[0]._fields 23 | assert points[0]._fields["GapToLeader"] == 44.163 24 | 25 | assert "IntervalToPositionAhead" in points[1]._fields 26 | assert points[1]._fields["IntervalToPositionAhead"] == 1.667 27 | 28 | 29 | def test_lapped_car(): 30 | msg = "['TimingData', {'Lines': {'20': {'GapToLeader': '1 L', 'IntervalToPositionAhead': {'Value': '+15.040'}}}}, '2023-03-05T16:08:38.509Z']" 31 | msg = fix_json(msg) 32 | cat, msg, dt = json.loads(msg) 33 | dt = to_datetime(dt) 34 | points = handle_message(cat, msg, dt) 35 | assert len(points) == 2 36 | assert points[0]._name == "gapToLeader" 37 | assert points[1]._name == "intervalToPositionAhead" 38 | 39 | assert "driver" in points[0]._tags 40 | assert points[0]._tags["driver"] == "MAG" 41 | 42 | 43 | def test_number_laps(): 44 | msg = "['TimingData', {'Lines': {'10': {'NumberOfLaps': 7, 'Sectors': {'2': {'Value': '25.040'}}, 'Speeds': {'FL': {'Value': '285'}}, 'LastLapTime': {'Value': '1:41.134'}}}}, '2023-03-05T15:15:33.534Z']" 45 | msg = fix_json(msg) 46 | cat, msg, dt = json.loads(msg) 47 | dt = to_datetime(dt) 48 | points = handle_message(cat, msg, dt) 49 | assert len(points) == 2 50 | assert points[1]._name == "numberOfLaps" 51 | 52 | assert "NumberOfLaps" in points[1]._fields 53 | assert points[1]._fields["NumberOfLaps"] == 7 54 | 55 | --------------------------------------------------------------------------------