ha-opensky-core 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. ha_opensky_core-0.1.0/LICENSE +21 -0
  2. ha_opensky_core-0.1.0/PKG-INFO +144 -0
  3. ha_opensky_core-0.1.0/README.md +124 -0
  4. ha_opensky_core-0.1.0/ha_opensky_core.egg-info/PKG-INFO +144 -0
  5. ha_opensky_core-0.1.0/ha_opensky_core.egg-info/SOURCES.txt +45 -0
  6. ha_opensky_core-0.1.0/ha_opensky_core.egg-info/dependency_links.txt +1 -0
  7. ha_opensky_core-0.1.0/ha_opensky_core.egg-info/requires.txt +1 -0
  8. ha_opensky_core-0.1.0/ha_opensky_core.egg-info/top_level.txt +1 -0
  9. ha_opensky_core-0.1.0/opensky/__init__.py +0 -0
  10. ha_opensky_core-0.1.0/opensky/adsbfi_metadata_provider.py +176 -0
  11. ha_opensky_core-0.1.0/opensky/aircraft_metadata.py +639 -0
  12. ha_opensky_core-0.1.0/opensky/airport.py +303 -0
  13. ha_opensky_core-0.1.0/opensky/airport_analyzer.py +266 -0
  14. ha_opensky_core-0.1.0/opensky/analyzer.py +410 -0
  15. ha_opensky_core-0.1.0/opensky/approach.py +142 -0
  16. ha_opensky_core-0.1.0/opensky/approach_history.py +145 -0
  17. ha_opensky_core-0.1.0/opensky/approach_manager.py +1063 -0
  18. ha_opensky_core-0.1.0/opensky/approach_report.py +132 -0
  19. ha_opensky_core-0.1.0/opensky/approach_repository.py +35 -0
  20. ha_opensky_core-0.1.0/opensky/approach_repository_memory.py +47 -0
  21. ha_opensky_core-0.1.0/opensky/approach_service.py +193 -0
  22. ha_opensky_core-0.1.0/opensky/approach_statistics.py +365 -0
  23. ha_opensky_core-0.1.0/opensky/approach_statistics_summary.py +66 -0
  24. ha_opensky_core-0.1.0/opensky/approach_summary.py +152 -0
  25. ha_opensky_core-0.1.0/opensky/approach_tracker.py +445 -0
  26. ha_opensky_core-0.1.0/opensky/auth.py +104 -0
  27. ha_opensky_core-0.1.0/opensky/client.py +58 -0
  28. ha_opensky_core-0.1.0/opensky/data/airports.csv +85846 -0
  29. ha_opensky_core-0.1.0/opensky/data/runways.csv +48147 -0
  30. ha_opensky_core-0.1.0/opensky/freshness.py +41 -0
  31. ha_opensky_core-0.1.0/opensky/geometry.py +187 -0
  32. ha_opensky_core-0.1.0/opensky/hexdb_metadata_provider.py +168 -0
  33. ha_opensky_core-0.1.0/opensky/metadata_normalizer.py +187 -0
  34. ha_opensky_core-0.1.0/opensky/models.py +209 -0
  35. ha_opensky_core-0.1.0/opensky/parser.py +42 -0
  36. ha_opensky_core-0.1.0/opensky/photo_provider.py +277 -0
  37. ha_opensky_core-0.1.0/opensky/planespotters_metadata_provider.py +287 -0
  38. ha_opensky_core-0.1.0/opensky/runway.py +230 -0
  39. ha_opensky_core-0.1.0/opensky/runways.py +29 -0
  40. ha_opensky_core-0.1.0/opensky/sqlite_approach_repository.py +320 -0
  41. ha_opensky_core-0.1.0/opensky/tests/__init__.py +0 -0
  42. ha_opensky_core-0.1.0/opensky/tests/test_aircraft_freshness.py +48 -0
  43. ha_opensky_core-0.1.0/opensky/tests/test_analyzer_freshness.py +156 -0
  44. ha_opensky_core-0.1.0/opensky/tests/test_freshness.py +43 -0
  45. ha_opensky_core-0.1.0/opensky/tracker.py +67 -0
  46. ha_opensky_core-0.1.0/pyproject.toml +44 -0
  47. ha_opensky_core-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stego020472
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.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: ha-opensky-core
3
+ Version: 0.1.0
4
+ Summary: Standalone OpenSky OAuth client and aircraft analysis library
5
+ Author: stego020472
6
+ License-Expression: MIT
7
+ Keywords: opensky,opensky-network,aviation,aircraft,ads-b,oauth,home-assistant
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.13
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.32.0
19
+ Dynamic: license-file
20
+
21
+ # ha-opensky-core
22
+
23
+ Standalone Python library for accessing the OpenSky Network API using OAuth authentication and for performing aircraft, airport and approach analysis.
24
+
25
+ The library is designed to be usable independently of Home Assistant. Home Assistant-specific functionality is implemented separately in the `ha-opensky-oauth` integration.
26
+
27
+ ## Features
28
+
29
+ * OpenSky Network OAuth authentication
30
+ * Search aircraft within a geographic area
31
+ * Aircraft data models using metric units
32
+ * Aircraft metadata lookup
33
+ * Aircraft photo lookup and fallback providers
34
+ * Airport and runway database
35
+ * Airport and runway analysis
36
+ * Aircraft approach detection and tracking
37
+ * Approach entry and exit transitions
38
+ * Reusable `ApproachService` boundary for frontend applications
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install ha-opensky-core
44
+ ```
45
+
46
+ ## Basic usage
47
+
48
+ ```python
49
+ from opensky.client import OpenSkyClient
50
+ from opensky.models import SearchArea
51
+
52
+ area = SearchArea(
53
+ latitude=50.901,
54
+ longitude=4.484,
55
+ radius_km=20,
56
+ )
57
+
58
+ client = OpenSkyClient(...)
59
+
60
+ aircraft = client.get_aircraft(area)
61
+
62
+ for item in aircraft:
63
+ print(item.icao24, item.callsign)
64
+ ```
65
+
66
+ The exact authentication and client configuration depends on the OpenSky OAuth credentials available to the application.
67
+
68
+ ## Airport database
69
+
70
+ The package contains the airport and runway databases required by the airport analysis functionality.
71
+
72
+ ```python
73
+ from opensky.airport import AirportDatabase
74
+
75
+ db = AirportDatabase()
76
+
77
+ print(db.count())
78
+
79
+ airport = db.require("EBBR")
80
+ print(airport.name)
81
+ ```
82
+
83
+ ## Architecture
84
+
85
+ `ha-opensky-core` contains the reusable OpenSky functionality and has no dependency on Home Assistant.
86
+
87
+ The architecture is intentionally split into two layers:
88
+
89
+ ```text
90
+ ┌──────────────────────────────────────┐
91
+ │ Home Assistant integration │
92
+ │ ha-opensky-oauth │
93
+ └──────────────────┬───────────────────┘
94
+ │
95
+ ▼
96
+ ┌──────────────────────────────────────┐
97
+ │ ha-opensky-core │
98
+ │ │
99
+ │ OAuth / API / Aircraft / Metadata │
100
+ │ Airports / Runways / Approaches │
101
+ └──────────────────┬───────────────────┘
102
+ │
103
+ ▼
104
+ OpenSky Network API
105
+ ```
106
+
107
+ This allows the core library to be tested and used independently of Home Assistant.
108
+
109
+ ## Development
110
+
111
+ Clone the repository and create a virtual environment:
112
+
113
+ ```bash
114
+ python -m venv .venv
115
+ ```
116
+
117
+ Activate the environment and install the package in editable mode:
118
+
119
+ ```bash
120
+ python -m pip install -e .
121
+ ```
122
+
123
+ Run the available test and diagnostic scripts with:
124
+
125
+ ```bash
126
+ python -m scripts.test_oauth
127
+ python -m scripts.test_metadata
128
+ python -m scripts.test_airports
129
+ ```
130
+
131
+ ## Requirements
132
+
133
+ * Python 3.13 or newer
134
+ * OpenSky Network OAuth credentials for authenticated API access
135
+
136
+ ## Status
137
+
138
+ This project is currently in alpha development.
139
+
140
+ The API may change between releases.
141
+
142
+ ## License
143
+
144
+ MIT License.
@@ -0,0 +1,124 @@
1
+ # ha-opensky-core
2
+
3
+ Standalone Python library for accessing the OpenSky Network API using OAuth authentication and for performing aircraft, airport and approach analysis.
4
+
5
+ The library is designed to be usable independently of Home Assistant. Home Assistant-specific functionality is implemented separately in the `ha-opensky-oauth` integration.
6
+
7
+ ## Features
8
+
9
+ * OpenSky Network OAuth authentication
10
+ * Search aircraft within a geographic area
11
+ * Aircraft data models using metric units
12
+ * Aircraft metadata lookup
13
+ * Aircraft photo lookup and fallback providers
14
+ * Airport and runway database
15
+ * Airport and runway analysis
16
+ * Aircraft approach detection and tracking
17
+ * Approach entry and exit transitions
18
+ * Reusable `ApproachService` boundary for frontend applications
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install ha-opensky-core
24
+ ```
25
+
26
+ ## Basic usage
27
+
28
+ ```python
29
+ from opensky.client import OpenSkyClient
30
+ from opensky.models import SearchArea
31
+
32
+ area = SearchArea(
33
+ latitude=50.901,
34
+ longitude=4.484,
35
+ radius_km=20,
36
+ )
37
+
38
+ client = OpenSkyClient(...)
39
+
40
+ aircraft = client.get_aircraft(area)
41
+
42
+ for item in aircraft:
43
+ print(item.icao24, item.callsign)
44
+ ```
45
+
46
+ The exact authentication and client configuration depends on the OpenSky OAuth credentials available to the application.
47
+
48
+ ## Airport database
49
+
50
+ The package contains the airport and runway databases required by the airport analysis functionality.
51
+
52
+ ```python
53
+ from opensky.airport import AirportDatabase
54
+
55
+ db = AirportDatabase()
56
+
57
+ print(db.count())
58
+
59
+ airport = db.require("EBBR")
60
+ print(airport.name)
61
+ ```
62
+
63
+ ## Architecture
64
+
65
+ `ha-opensky-core` contains the reusable OpenSky functionality and has no dependency on Home Assistant.
66
+
67
+ The architecture is intentionally split into two layers:
68
+
69
+ ```text
70
+ ┌──────────────────────────────────────┐
71
+ │ Home Assistant integration │
72
+ │ ha-opensky-oauth │
73
+ └──────────────────┬───────────────────┘
74
+ │
75
+ ▼
76
+ ┌──────────────────────────────────────┐
77
+ │ ha-opensky-core │
78
+ │ │
79
+ │ OAuth / API / Aircraft / Metadata │
80
+ │ Airports / Runways / Approaches │
81
+ └──────────────────┬───────────────────┘
82
+ │
83
+ ▼
84
+ OpenSky Network API
85
+ ```
86
+
87
+ This allows the core library to be tested and used independently of Home Assistant.
88
+
89
+ ## Development
90
+
91
+ Clone the repository and create a virtual environment:
92
+
93
+ ```bash
94
+ python -m venv .venv
95
+ ```
96
+
97
+ Activate the environment and install the package in editable mode:
98
+
99
+ ```bash
100
+ python -m pip install -e .
101
+ ```
102
+
103
+ Run the available test and diagnostic scripts with:
104
+
105
+ ```bash
106
+ python -m scripts.test_oauth
107
+ python -m scripts.test_metadata
108
+ python -m scripts.test_airports
109
+ ```
110
+
111
+ ## Requirements
112
+
113
+ * Python 3.13 or newer
114
+ * OpenSky Network OAuth credentials for authenticated API access
115
+
116
+ ## Status
117
+
118
+ This project is currently in alpha development.
119
+
120
+ The API may change between releases.
121
+
122
+ ## License
123
+
124
+ MIT License.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: ha-opensky-core
3
+ Version: 0.1.0
4
+ Summary: Standalone OpenSky OAuth client and aircraft analysis library
5
+ Author: stego020472
6
+ License-Expression: MIT
7
+ Keywords: opensky,opensky-network,aviation,aircraft,ads-b,oauth,home-assistant
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.13
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests>=2.32.0
19
+ Dynamic: license-file
20
+
21
+ # ha-opensky-core
22
+
23
+ Standalone Python library for accessing the OpenSky Network API using OAuth authentication and for performing aircraft, airport and approach analysis.
24
+
25
+ The library is designed to be usable independently of Home Assistant. Home Assistant-specific functionality is implemented separately in the `ha-opensky-oauth` integration.
26
+
27
+ ## Features
28
+
29
+ * OpenSky Network OAuth authentication
30
+ * Search aircraft within a geographic area
31
+ * Aircraft data models using metric units
32
+ * Aircraft metadata lookup
33
+ * Aircraft photo lookup and fallback providers
34
+ * Airport and runway database
35
+ * Airport and runway analysis
36
+ * Aircraft approach detection and tracking
37
+ * Approach entry and exit transitions
38
+ * Reusable `ApproachService` boundary for frontend applications
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install ha-opensky-core
44
+ ```
45
+
46
+ ## Basic usage
47
+
48
+ ```python
49
+ from opensky.client import OpenSkyClient
50
+ from opensky.models import SearchArea
51
+
52
+ area = SearchArea(
53
+ latitude=50.901,
54
+ longitude=4.484,
55
+ radius_km=20,
56
+ )
57
+
58
+ client = OpenSkyClient(...)
59
+
60
+ aircraft = client.get_aircraft(area)
61
+
62
+ for item in aircraft:
63
+ print(item.icao24, item.callsign)
64
+ ```
65
+
66
+ The exact authentication and client configuration depends on the OpenSky OAuth credentials available to the application.
67
+
68
+ ## Airport database
69
+
70
+ The package contains the airport and runway databases required by the airport analysis functionality.
71
+
72
+ ```python
73
+ from opensky.airport import AirportDatabase
74
+
75
+ db = AirportDatabase()
76
+
77
+ print(db.count())
78
+
79
+ airport = db.require("EBBR")
80
+ print(airport.name)
81
+ ```
82
+
83
+ ## Architecture
84
+
85
+ `ha-opensky-core` contains the reusable OpenSky functionality and has no dependency on Home Assistant.
86
+
87
+ The architecture is intentionally split into two layers:
88
+
89
+ ```text
90
+ ┌──────────────────────────────────────┐
91
+ │ Home Assistant integration │
92
+ │ ha-opensky-oauth │
93
+ └──────────────────┬───────────────────┘
94
+ │
95
+ ▼
96
+ ┌──────────────────────────────────────┐
97
+ │ ha-opensky-core │
98
+ │ │
99
+ │ OAuth / API / Aircraft / Metadata │
100
+ │ Airports / Runways / Approaches │
101
+ └──────────────────┬───────────────────┘
102
+ │
103
+ ▼
104
+ OpenSky Network API
105
+ ```
106
+
107
+ This allows the core library to be tested and used independently of Home Assistant.
108
+
109
+ ## Development
110
+
111
+ Clone the repository and create a virtual environment:
112
+
113
+ ```bash
114
+ python -m venv .venv
115
+ ```
116
+
117
+ Activate the environment and install the package in editable mode:
118
+
119
+ ```bash
120
+ python -m pip install -e .
121
+ ```
122
+
123
+ Run the available test and diagnostic scripts with:
124
+
125
+ ```bash
126
+ python -m scripts.test_oauth
127
+ python -m scripts.test_metadata
128
+ python -m scripts.test_airports
129
+ ```
130
+
131
+ ## Requirements
132
+
133
+ * Python 3.13 or newer
134
+ * OpenSky Network OAuth credentials for authenticated API access
135
+
136
+ ## Status
137
+
138
+ This project is currently in alpha development.
139
+
140
+ The API may change between releases.
141
+
142
+ ## License
143
+
144
+ MIT License.
@@ -0,0 +1,45 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ha_opensky_core.egg-info/PKG-INFO
5
+ ha_opensky_core.egg-info/SOURCES.txt
6
+ ha_opensky_core.egg-info/dependency_links.txt
7
+ ha_opensky_core.egg-info/requires.txt
8
+ ha_opensky_core.egg-info/top_level.txt
9
+ opensky/__init__.py
10
+ opensky/adsbfi_metadata_provider.py
11
+ opensky/aircraft_metadata.py
12
+ opensky/airport.py
13
+ opensky/airport_analyzer.py
14
+ opensky/analyzer.py
15
+ opensky/approach.py
16
+ opensky/approach_history.py
17
+ opensky/approach_manager.py
18
+ opensky/approach_report.py
19
+ opensky/approach_repository.py
20
+ opensky/approach_repository_memory.py
21
+ opensky/approach_service.py
22
+ opensky/approach_statistics.py
23
+ opensky/approach_statistics_summary.py
24
+ opensky/approach_summary.py
25
+ opensky/approach_tracker.py
26
+ opensky/auth.py
27
+ opensky/client.py
28
+ opensky/freshness.py
29
+ opensky/geometry.py
30
+ opensky/hexdb_metadata_provider.py
31
+ opensky/metadata_normalizer.py
32
+ opensky/models.py
33
+ opensky/parser.py
34
+ opensky/photo_provider.py
35
+ opensky/planespotters_metadata_provider.py
36
+ opensky/runway.py
37
+ opensky/runways.py
38
+ opensky/sqlite_approach_repository.py
39
+ opensky/tracker.py
40
+ opensky/data/airports.csv
41
+ opensky/data/runways.csv
42
+ opensky/tests/__init__.py
43
+ opensky/tests/test_aircraft_freshness.py
44
+ opensky/tests/test_analyzer_freshness.py
45
+ opensky/tests/test_freshness.py
@@ -0,0 +1 @@
1
+ requests>=2.32.0
File without changes
@@ -0,0 +1,176 @@
1
+ """
2
+ ADSB.fi aircraft metadata provider.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+
10
+ import requests
11
+
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ ADSBFI_AIRCRAFT_URL = (
17
+ "https://opendata.adsb.fi/api/v2/hex/{icao24}"
18
+ )
19
+
20
+ ADSBFI_USER_AGENT = (
21
+ "ha-opensky-oauth/1.0 "
22
+ "(https://github.com/stego020472/ha-opensky-oauth)"
23
+ )
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class AdsbFiMetadata:
28
+ """Aircraft metadata returned by ADSB.fi."""
29
+
30
+ registration: str | None = None
31
+ aircraft_type: str | None = None
32
+ icao_type: str | None = None
33
+ manufacturer: str | None = None
34
+
35
+
36
+ class AdsbFiMetadataProvider:
37
+ """
38
+ Retrieve aircraft metadata from ADSB.fi.
39
+
40
+ ADSB.fi is used as a metadata fallback when the
41
+ primary metadata providers do not have sufficient
42
+ information.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ timeout: float = 10.0,
48
+ ) -> None:
49
+ """Initialize the provider."""
50
+
51
+ self.timeout = timeout
52
+
53
+ def get(
54
+ self,
55
+ icao24: str | None = None,
56
+ ) -> AdsbFiMetadata | None:
57
+ """
58
+ Retrieve aircraft metadata using ICAO24.
59
+ """
60
+
61
+ if not icao24:
62
+ return None
63
+
64
+ icao24 = icao24.strip().lower()
65
+
66
+ if not icao24:
67
+ return None
68
+
69
+ url = ADSBFI_AIRCRAFT_URL.format(
70
+ icao24=icao24,
71
+ )
72
+
73
+ return self._request(url)
74
+
75
+ def _request(
76
+ self,
77
+ url: str,
78
+ ) -> AdsbFiMetadata | None:
79
+ """Request and parse an ADSB.fi response."""
80
+
81
+ try:
82
+ response = requests.get(
83
+ url,
84
+ headers={
85
+ "User-Agent": ADSBFI_USER_AGENT,
86
+ },
87
+ timeout=self.timeout,
88
+ )
89
+
90
+ except requests.exceptions.Timeout:
91
+ _LOGGER.debug(
92
+ "ADSB.fi request timed out: %s",
93
+ url,
94
+ )
95
+ return None
96
+
97
+ except requests.exceptions.ConnectionError:
98
+ _LOGGER.debug(
99
+ "Unable to connect to ADSB.fi: %s",
100
+ url,
101
+ )
102
+ return None
103
+
104
+ except requests.exceptions.RequestException as exc:
105
+ _LOGGER.debug(
106
+ "ADSB.fi request failed: %s",
107
+ exc,
108
+ )
109
+ return None
110
+
111
+ if response.status_code != 200:
112
+ _LOGGER.debug(
113
+ "ADSB.fi returned HTTP %s",
114
+ response.status_code,
115
+ )
116
+ return None
117
+
118
+ try:
119
+ data = response.json()
120
+
121
+ except ValueError:
122
+ _LOGGER.debug(
123
+ "ADSB.fi returned invalid JSON"
124
+ )
125
+ return None
126
+
127
+ if not isinstance(data, dict):
128
+ return None
129
+
130
+ aircraft = data.get("ac")
131
+
132
+ if not isinstance(aircraft, list):
133
+ return None
134
+
135
+ if not aircraft:
136
+ return None
137
+
138
+ first_aircraft = aircraft[0]
139
+
140
+ if not isinstance(
141
+ first_aircraft,
142
+ dict,
143
+ ):
144
+ return None
145
+
146
+ return AdsbFiMetadata(
147
+ registration=self._string(
148
+ first_aircraft.get("r")
149
+ ),
150
+ aircraft_type=self._string(
151
+ first_aircraft.get("desc")
152
+ ),
153
+ icao_type=self._string(
154
+ first_aircraft.get("t")
155
+ ),
156
+ manufacturer=None,
157
+ )
158
+
159
+ @staticmethod
160
+ def _string(
161
+ value: object,
162
+ ) -> str | None:
163
+ """Return a cleaned string or None."""
164
+
165
+ if not isinstance(
166
+ value,
167
+ str,
168
+ ):
169
+ return None
170
+
171
+ value = value.strip()
172
+
173
+ if not value:
174
+ return None
175
+
176
+ return value