Skip to content

Commit 7710ec3

Browse files
authored
Merge pull request #52 from CoMPaTech/validate_fixture
Fix age field in Remote for nanostation 8.7.11 Refactor Improved the fixture generation script for better maintainability and dynamic processing of JSON files. Updated internal data structure inheritance for consistency. Style Expanded numeric fields in several data classes to accept integer and null values for improved flexibility.
2 parents e82cb88 + b626c19 commit 7710ec3

File tree

4 files changed

+168
-83
lines changed

4 files changed

+168
-83
lines changed

airos/data.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ def _redact(d: dict):
9292
# Data class start
9393

9494

95+
class AirOSDataClass(DataClassDictMixin):
96+
"""A base class for all mashumaro dataclasses."""
97+
98+
pass
99+
100+
95101
def _check_and_log_unknown_enum_value(
96102
data_dict: dict[str, Any],
97103
key: str,
@@ -149,15 +155,15 @@ class NetRole(Enum):
149155

150156

151157
@dataclass
152-
class ChainName:
158+
class ChainName(AirOSDataClass):
153159
"""Leaf definition."""
154160

155161
number: int
156162
name: str
157163

158164

159165
@dataclass
160-
class Host:
166+
class Host(AirOSDataClass):
161167
"""Leaf definition."""
162168

163169
hostname: str
@@ -169,11 +175,11 @@ class Host:
169175
fwversion: str
170176
devmodel: str
171177
netrole: NetRole
172-
loadavg: float
178+
loadavg: float | int | None
173179
totalram: int
174180
freeram: int
175181
temperature: int
176-
cpuload: float
182+
cpuload: float | int | None
177183
height: int | None # Reported none on LiteBeam 5AC
178184

179185
@classmethod
@@ -184,7 +190,7 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
184190

185191

186192
@dataclass
187-
class Services:
193+
class Services(AirOSDataClass):
188194
"""Leaf definition."""
189195

190196
dhcpc: bool
@@ -195,7 +201,7 @@ class Services:
195201

196202

197203
@dataclass
198-
class Firewall:
204+
class Firewall(AirOSDataClass):
199205
"""Leaf definition."""
200206

201207
iptables: bool
@@ -205,23 +211,23 @@ class Firewall:
205211

206212

207213
@dataclass
208-
class Throughput:
214+
class Throughput(AirOSDataClass):
209215
"""Leaf definition."""
210216

211217
tx: int
212218
rx: int
213219

214220

215221
@dataclass
216-
class ServiceTime:
222+
class ServiceTime(AirOSDataClass):
217223
"""Leaf definition."""
218224

219225
time: int
220226
link: int
221227

222228

223229
@dataclass
224-
class Polling:
230+
class Polling(AirOSDataClass):
225231
"""Leaf definition."""
226232

227233
cb_capacity: int
@@ -238,7 +244,7 @@ class Polling:
238244

239245

240246
@dataclass
241-
class Stats:
247+
class Stats(AirOSDataClass):
242248
"""Leaf definition."""
243249

244250
rx_bytes: int
@@ -250,7 +256,7 @@ class Stats:
250256

251257

252258
@dataclass
253-
class EvmData:
259+
class EvmData(AirOSDataClass):
254260
"""Leaf definition."""
255261

256262
usage: int
@@ -259,7 +265,7 @@ class EvmData:
259265

260266

261267
@dataclass
262-
class Airmax:
268+
class Airmax(AirOSDataClass):
263269
"""Leaf definition."""
264270

265271
actual_priority: int
@@ -274,7 +280,7 @@ class Airmax:
274280

275281

276282
@dataclass
277-
class EthList:
283+
class EthList(AirOSDataClass):
278284
"""Leaf definition."""
279285

280286
ifname: str
@@ -287,38 +293,37 @@ class EthList:
287293

288294

289295
@dataclass
290-
class GPSData:
296+
class GPSData(AirOSDataClass):
291297
"""Leaf definition."""
292298

293-
lat: float | None = None
294-
lon: float | None = None
299+
lat: float | int | None = None
300+
lon: float | int | None = None
295301
fix: int | None = None
296302
sats: int | None = None # LiteAP GPS
297303
dim: int | None = None # LiteAP GPS
298-
dop: float | None = None # LiteAP GPS
299-
alt: float | None = None # LiteAP GPS
304+
dop: float | int | None = None # LiteAP GPS
305+
alt: float | int | None = None # LiteAP GPS
300306
time_synced: int | None = None # LiteAP GPS
301307

302308

303309
@dataclass
304-
class UnmsStatus:
310+
class UnmsStatus(AirOSDataClass):
305311
"""Leaf definition."""
306312

307313
status: int
308314
timestamp: str | None = None
309315

310316

311317
@dataclass
312-
class Remote:
318+
class Remote(AirOSDataClass):
313319
"""Leaf definition."""
314320

315-
age: int
316321
device_id: str
317322
hostname: str
318323
platform: str
319324
version: str
320325
time: str
321-
cpuload: float
326+
cpuload: float | int | None
322327
temperature: int
323328
totalram: int
324329
freeram: int
@@ -351,6 +356,7 @@ class Remote:
351356
mode: WirelessMode | None = None # Investigate why remotes can have no mode set
352357
ip6addr: list[str] | None = None # For v4 only devices
353358
height: int | None = None
359+
age: int | None = None # At least not present on 8.7.11
354360

355361
@classmethod
356362
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
@@ -360,7 +366,7 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
360366

361367

362368
@dataclass
363-
class Disconnected:
369+
class Disconnected(AirOSDataClass):
364370
"""Leaf definition for disconnected devices."""
365371

366372
mac: str
@@ -374,7 +380,7 @@ class Disconnected:
374380

375381

376382
@dataclass
377-
class Station:
383+
class Station(AirOSDataClass):
378384
"""Leaf definition for connected/active devices."""
379385

380386
mac: str
@@ -413,7 +419,7 @@ class Station:
413419

414420

415421
@dataclass
416-
class Wireless:
422+
class Wireless(AirOSDataClass):
417423
"""Leaf definition."""
418424

419425
essid: str
@@ -465,7 +471,7 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
465471

466472

467473
@dataclass
468-
class InterfaceStatus:
474+
class InterfaceStatus(AirOSDataClass):
469475
"""Leaf definition."""
470476

471477
plugged: bool
@@ -486,7 +492,7 @@ class InterfaceStatus:
486492

487493

488494
@dataclass
489-
class Interface:
495+
class Interface(AirOSDataClass):
490496
"""Leaf definition."""
491497

492498
ifname: str
@@ -497,30 +503,30 @@ class Interface:
497503

498504

499505
@dataclass
500-
class ProvisioningMode:
506+
class ProvisioningMode(AirOSDataClass):
501507
"""Leaf definition."""
502508

503509
pass
504510

505511

506512
@dataclass
507-
class NtpClient:
513+
class NtpClient(AirOSDataClass):
508514
"""Leaf definition."""
509515

510516
pass
511517

512518

513519
@dataclass
514-
class GPSMain:
520+
class GPSMain(AirOSDataClass):
515521
"""Leaf definition."""
516522

517-
lat: float
518-
lon: float
523+
lat: float | int | None
524+
lon: float | int | None
519525
fix: int
520526

521527

522528
@dataclass
523-
class Derived:
529+
class Derived(AirOSDataClass):
524530
"""Contain custom data generated by this module."""
525531

526532
mac: str # Base device MAC address (i.e. eth0)
@@ -536,7 +542,7 @@ class Derived:
536542

537543

538544
@dataclass
539-
class AirOS8Data(DataClassDictMixin):
545+
class AirOS8Data(AirOSDataClass):
540546
"""Dataclass for AirOS v8 devices."""
541547

542548
chain_names: list[ChainName]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "airos"
7-
version = "0.2.6"
7+
version = "0.2.7"
88
license = "MIT"
99
description = "Ubiquity airOS module(s) for Python 3."
1010
readme = "README.md"

script/generate_ha_fixture.py

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Generate mock airos fixture for testing."""
1+
"""Generate mock airos fixtures for testing."""
22

33
import json
44
import logging
@@ -13,52 +13,53 @@
1313
if project_root_dir not in sys.path:
1414
sys.path.append(project_root_dir)
1515

16+
# NOTE: This assumes the airos module is correctly installed or available in the project path.
17+
# If not, you might need to adjust the import statement.
1618
from airos.airos8 import AirOS, AirOSData # noqa: E402
1719

18-
# Define the path to save the fixture
19-
fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures")
20-
userdata_dir = os.path.join(os.path.dirname(__file__), "../fixtures/userdata")
21-
new_fixture_path = os.path.join(fixture_dir, "airos_loco5ac_ap-ptp.json")
22-
base_fixture_path = os.path.join(userdata_dir, "loco5ac_ap-ptp.json")
23-
24-
with open(base_fixture_path) as source, open(new_fixture_path, "w") as new:
25-
source_data = json.loads(source.read())
26-
derived_data = AirOS.derived_data(None, source_data)
27-
new_data = AirOSData.from_dict(derived_data)
28-
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
29-
30-
new_fixture_path = os.path.join(fixture_dir, "airos_loco5ac_sta-ptp.json")
31-
base_fixture_path = os.path.join(userdata_dir, "loco5ac_sta-ptp.json")
32-
33-
with open(base_fixture_path) as source, open(new_fixture_path, "w") as new:
34-
source_data = json.loads(source.read())
35-
derived_data = AirOS.derived_data(None, source_data)
36-
new_data = AirOSData.from_dict(derived_data)
37-
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
38-
39-
new_fixture_path = os.path.join(fixture_dir, "airos_mocked_sta-ptmp.json")
40-
base_fixture_path = os.path.join(userdata_dir, "mocked_sta-ptmp.json")
41-
42-
with open(base_fixture_path) as source, open(new_fixture_path, "w") as new:
43-
source_data = json.loads(source.read())
44-
derived_data = AirOS.derived_data(None, source_data)
45-
new_data = AirOSData.from_dict(derived_data)
46-
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
47-
48-
new_fixture_path = os.path.join(fixture_dir, "airos_liteapgps_ap_ptmp_40mhz.json")
49-
base_fixture_path = os.path.join(userdata_dir, "liteapgps_ap_ptmp_40mhz.json")
50-
51-
with open(base_fixture_path) as source, open(new_fixture_path, "w") as new:
52-
source_data = json.loads(source.read())
53-
derived_data = AirOS.derived_data(None, source_data)
54-
new_data = AirOSData.from_dict(derived_data)
55-
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
56-
57-
new_fixture_path = os.path.join(fixture_dir, "airos_nanobeam5ac_sta_ptmp_40mhz.json")
58-
base_fixture_path = os.path.join(userdata_dir, "nanobeam5ac_sta_ptmp_40mhz.json")
59-
60-
with open(base_fixture_path) as source, open(new_fixture_path, "w") as new:
61-
source_data = json.loads(source.read())
62-
derived_data = AirOS.derived_data(None, source_data)
63-
new_data = AirOSData.from_dict(derived_data)
64-
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
20+
21+
def generate_airos_fixtures():
22+
"""Process all (intended) JSON files from the userdata directory to potential fixtures."""
23+
24+
# Define the paths to the directories
25+
fixture_dir = os.path.join(os.path.dirname(__file__), "../fixtures")
26+
userdata_dir = os.path.join(os.path.dirname(__file__), "../fixtures/userdata")
27+
28+
# Ensure the fixture directory exists
29+
os.makedirs(fixture_dir, exist_ok=True)
30+
31+
# Iterate over all files in the userdata_dir
32+
for filename in os.listdir(userdata_dir):
33+
if "mocked" in filename:
34+
continue
35+
if filename.endswith(".json"):
36+
# Construct the full paths for the base and new fixtures
37+
base_fixture_path = os.path.join(userdata_dir, filename)
38+
new_filename = f"airos_{filename}"
39+
new_fixture_path = os.path.join(fixture_dir, new_filename)
40+
41+
_LOGGER.info("Processing '%s'...", filename)
42+
43+
try:
44+
with open(base_fixture_path) as source:
45+
source_data = json.loads(source.read())
46+
47+
derived_data = AirOS.derived_data(None, source_data)
48+
new_data = AirOSData.from_dict(derived_data)
49+
50+
with open(new_fixture_path, "w") as new:
51+
json.dump(new_data.to_dict(), new, indent=2, sort_keys=True)
52+
53+
_LOGGER.info("Successfully created '%s'", new_filename)
54+
55+
except json.JSONDecodeError:
56+
_LOGGER.error("Skipping '%s': Not a valid JSON file.", filename)
57+
except Exception as e:
58+
_LOGGER.error("Error processing '%s': %s", filename, e)
59+
60+
61+
if __name__ == "__main__":
62+
logging.basicConfig(
63+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
64+
)
65+
generate_airos_fixtures()

0 commit comments

Comments
 (0)