Skip to content

Commit b985161

Browse files
author
Karthik Satheesh Kumar
committed
Refactor device metrics export to conditionally include location data in outputs based on command line arguments
1 parent a6ff5c7 commit b985161

3 files changed

Lines changed: 69 additions & 62 deletions

File tree

‎device-metrics-export/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ CSV / JSON fields (ordered for CSV; JSON objects contain the same keys):
123123
> [!TIP]
124124
> The Device Group field will only be populated for groups that are Central compatible device groups. Classic Central groups will not appear in this export.
125125

126-
> Location fields from Floorplan ID through Longitude are populated only when `--include-floorplan` (or `--include-raw-location`) is provided. The Raw Location column is additionally gated behind `--include-raw-location`; passing this flag also implies `--include-floorplan`, so no separate flag is needed. All other location columns remain present with blank/null values when location data is unavailable.
126+
> Location fields from Floorplan ID through Longitude are only added to the CSV/JSON when `--include-floorplan` (or `--include-raw-location`) is provided. The Raw Location column is additionally gated behind `--include-raw-location`; passing this flag also implies `--include-floorplan`. When neither flag is provided, location columns are omitted entirely from outputs.
127127

128128

129129
If no data is returned from the APIs or post-processing yields an empty list, the script prints "No data to save."
@@ -132,7 +132,7 @@ If no data is returned from the APIs or post-processing yields an empty list, th
132132

133133
- Authentication / tokens: Ensure your credentials file contains a top-level `unified` section with `workspace_id`, `client_id`, `client_secret`, and `cluster_name` or `base_url`.
134134
- Empty output: Confirm that devices exist in Central inventory and that GLP subscriptions/devices exist for the account.
135-
- Missing location fields: Confirm APs have `siteId` values and floorplan/device-location data exists for the site. Devices remain in the output with blank/null location fields when location data is unavailable.
135+
-- Missing location fields: Confirm APs have `siteId` values and floorplan/device-location data exists for the site. If you want location columns in the output, run with `--include-floorplan` (or `--include-raw-location`).
136136
- SDK compatibility: If API calls fail unexpectedly, confirm the installed pycentral version matches tested versions (v2.0a22) or update helpers accordingly.
137137

138138
## Support

‎device-metrics-export/device_metrics_export.py‎

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@
1111
process_list,
1212
process_glp_device,
1313
processed_data,
14-
ensure_tokens_available,
1514
get_all_device_inventory,
1615
derive_site_ids_with_aps,
1716
fetch_device_locations,
1817
)
1918

20-
CSV_COLUMNS = [
19+
BASE_COLUMNS = [
2120
# Device information columns
2221
"Serial Number",
2322
"Mac Address",
@@ -40,29 +39,27 @@
4039
"Subscription Tier",
4140
"Subscription Type",
4241
"Subscription End Time",
43-
# Location fields (may be empty)
42+
]
43+
44+
LOCATION_COLUMNS = [
4445
"Floorplan ID",
4546
"Building ID",
4647
"X Coordinate",
4748
"Y Coordinate",
4849
"Floor Coordinate Unit",
4950
"Latitude",
5051
"Longitude",
51-
"Raw Location",
5252
]
5353

54+
RAW_LOCATION_COLUMN = "Raw Location"
55+
5456
LOCATION_FETCH_WORKERS = 3
5557

5658

5759
def main():
5860
args = parse_args()
5961
new_central_conn = NewCentralBase(token_info=args.credentials, log_level="ERROR")
60-
try:
61-
ensure_tokens_available(new_central_conn)
62-
except Exception as e:
63-
print(f"Error: {e}")
64-
return
65-
62+
6663
devices_api = Devices()
6764
subscriptions_api = Subscriptions()
6865

@@ -95,25 +92,38 @@ def main():
9592
else:
9693
print("No site IDs with APs found; skipping floorplan/device-location fetch.")
9794

98-
processed = process_all_data(raw_data, processed_locations)
95+
processed = process_all_data(
96+
raw_data,
97+
processed_locations,
98+
processed_inventory,
99+
processed_devices,
100+
)
99101

100102
# Save results to CSV and structured JSON
101-
output = processed_data(**processed)
102-
effective_columns = (
103-
CSV_COLUMNS
104-
if args.include_raw_location
105-
else [col for col in CSV_COLUMNS if col != "Raw Location"]
103+
output = processed_data(
104+
**processed,
105+
include_floorplan=(args.include_floorplan or args.include_raw_location),
106+
include_raw_location=args.include_raw_location,
106107
)
108+
109+
# Build columns based on flags: omit location columns unless requested
110+
effective_columns = list(BASE_COLUMNS)
111+
if args.include_floorplan or args.include_raw_location:
112+
effective_columns += LOCATION_COLUMNS
113+
if args.include_raw_location:
114+
effective_columns.append(RAW_LOCATION_COLUMN)
107115
if output:
108116
# Prepare CSV rows: ensure Raw Location is serialized to a JSON string
109117
csv_rows = []
110118
for row in output:
111119
r = row.copy()
112-
raw = r.get("Raw Location", "")
113-
if isinstance(raw, (dict, list)):
114-
r["Raw Location"] = json.dumps(raw, ensure_ascii=False)
115-
elif raw is None:
116-
r["Raw Location"] = ""
120+
# only serialize Raw Location when it's part of effective columns
121+
if RAW_LOCATION_COLUMN in effective_columns:
122+
raw = r.get(RAW_LOCATION_COLUMN, "")
123+
if isinstance(raw, (dict, list)):
124+
r[RAW_LOCATION_COLUMN] = json.dumps(raw, ensure_ascii=False)
125+
elif raw is None:
126+
r[RAW_LOCATION_COLUMN] = ""
117127
csv_rows.append(r)
118128

119129
df = pd.DataFrame(csv_rows)
@@ -199,15 +209,24 @@ def validate_file_format(file_path):
199209
return file_path
200210

201211

202-
def process_all_data(raw_data, processed_locations=None):
203-
"""Process raw API data into structured format. Accept optional processed_locations map."""
204-
processed_inventory = process_list(
205-
_safe("device_inventory", raw_data), "serialNumber"
206-
)
212+
def process_all_data(
213+
raw_data, processed_locations=None, processed_inventory=None, processed_devices=None
214+
):
215+
"""Process raw API data into structured format.
216+
217+
Accept optional precomputed `processed_inventory` and `processed_devices` to
218+
avoid recomputing large mappings when callers already have them.
219+
"""
220+
# compute only if not provided to preserve backward compatibility
221+
if processed_inventory is None:
222+
processed_inventory = process_list(
223+
_safe("device_inventory", raw_data), "serialNumber"
224+
)
225+
if processed_devices is None:
226+
processed_devices = process_monitoring_data(_safe("monitoring_devices", raw_data))
227+
207228
return {
208-
"processed_devices": process_monitoring_data(
209-
_safe("monitoring_devices", raw_data)
210-
),
229+
"processed_devices": processed_devices,
211230
"processed_inventory": processed_inventory,
212231
"processed_glp_devices": process_glp_device(
213232
process_list(_safe("glp_devices", raw_data), "serialNumber"),

‎device-metrics-export/utils.py‎

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ def processed_data(
6565
processed_glp_devices=None,
6666
processed_subs=None,
6767
processed_locations=None,
68+
include_floorplan: bool = False,
69+
include_raw_location: bool = False,
6870
):
6971
"""Build list of device dicts for CSV output.
7072
@@ -126,41 +128,27 @@ def processed_data(
126128
"Subscription End Time": iso_to_human(sub.get("endTime", "")),
127129
}
128130
)
129-
# attach location fields if present (do not omit rows if missing)
130-
loc = processed_locations.get(serial_number, {}) or {}
131-
entry.update(
132-
{
133-
"Floorplan ID": loc.get("floorplan_id", ""),
134-
"Building ID": loc.get("building_id", ""),
135-
"X Coordinate": loc.get("floor_x", ""),
136-
"Y Coordinate": loc.get("floor_y", ""),
137-
"Floor Coordinate Unit": loc.get("floor_coordinate_unit", ""),
138-
"Latitude": loc.get("device_latitude", ""),
139-
"Longitude": loc.get("device_longitude", ""),
140-
"Raw Location": loc.get("raw_location", ""),
141-
}
142-
)
131+
# attach location fields only when requested (do not omit rows if missing)
132+
if include_floorplan:
133+
loc = processed_locations.get(serial_number, {}) or {}
134+
entry.update(
135+
{
136+
"Floorplan ID": loc.get("floorplan_id", ""),
137+
"Building ID": loc.get("building_id", ""),
138+
"X Coordinate": loc.get("floor_x", ""),
139+
"Y Coordinate": loc.get("floor_y", ""),
140+
"Floor Coordinate Unit": loc.get("floor_coordinate_unit", ""),
141+
"Latitude": loc.get("device_latitude", ""),
142+
"Longitude": loc.get("device_longitude", ""),
143+
}
144+
)
145+
if include_raw_location:
146+
# include raw_location (may be dict) only when explicitly requested
147+
entry["Raw Location"] = loc.get("raw_location", "")
143148
result.append(entry)
144149
return result
145150

146151

147-
def ensure_tokens_available(new_central_conn):
148-
"""Ensure unified token information can reach Central and GLP APIs."""
149-
tokens = getattr(new_central_conn, "token_info", None)
150-
if not tokens or "unified" not in tokens:
151-
raise Exception(
152-
"Unified token information is missing. Provide a valid PyCentral "
153-
"unified credentials file with client_id, client_secret, workspace_id, "
154-
"and cluster_name or base_url."
155-
)
156-
routes = getattr(new_central_conn, "_app_routes", {})
157-
missing_routes = {"new_central", "glp"} - set(routes)
158-
if missing_routes:
159-
raise Exception(
160-
"Unified credentials must enable both Central and GLP API routes. "
161-
"Provide workspace_id and cluster_name or base_url under unified."
162-
)
163-
return True
164152

165153

166154
def get_all_device_inventory(MonitoringDevices, central_conn):

0 commit comments

Comments
 (0)