Here's a quick overview and most of the code I have stripped out that is relevant. It was a big learning curve so my code might be poorly written. When I have time I'll make it better.
My actual setup is an AWS server using Docker containers. I have the following containers running.
Flask - Publishing the web pages
Grafana - Displaying historical data for all Meraki devices and other network devices.
InflxDb - Storing the data for above
Python3 - For my main code
Busybox - Used for sharing data between containers
=========================================================================
Flask code. The URL for the map will be http://<your server>/map
getOrganizationDeviceStatuses_with_location.json is from your python code generated from script 2
You'll need a sample index.html for your home or remove the route to it.
I've stripped out loads of code as I do a lot more than just maps. I do health reporting too but that would be too hard to explain how it all hangs together. One day I might write a doco.
There's probably imports not needed for a simple map in the python code but you'll have to work some stuff out. You'll need to setup directory structures and have an understanding of python and html etc to get it working but it'll give you an idea on how it works.
=========================================================================
from flask import Flask, render_template, request, jsonify, send_file, send_from_directory
import json
import os
import datetime
import pytz
import csv
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
@app.route('/map')
def map_page():
data_url = "http://<your server>/shared_data/getOrganizationDeviceStatuses_with_location.json"
return render_template('map.html', data_url=data_url)
Python code - 2 scripts I use as I do other stuff with the first API results. can be combined into one script. i collect data every 5 minutes in a loop.
JSON_store is the place I store the JSON data in the busybox container
==============
SCRIPT 1 - This part grabs the devices from the ORG and the Device statuses and writes to a JSON store
==============
response = dashboard.organizations.getOrganizationDevices(org_id, total_pages='all')
with open(JSON_store + "getOrganizationDevices.json", "w") as f:
json.dump(response, f)
response = dashboard.organizations.getOrganizationDevicesStatuses(org_id, total_pages='all')
with open(JSON_store + "getOrganizationDevicesStatuses.json", "w") as f:
json.dump(response, f)
===========
SCRIPT 2
===========
import json
# Read the first JSON file
with open('json_files/getOrganizationDevices.json', 'r') as file:
devices = json.load(file)
# Read the second JSON file
with open('json_files/getOrganizationDevicesStatuses.json', 'r') as file:
statuses = json.load(file)
# Create a dictionary to store the device status with location
devices_with_location = {}
# Iterate over the devices
for device in devices:
name = device['name']
if 'nomad' in name.lower() or 'lab' in name.lower():
continue # Skip this device if it contains "nomad" or "lab" in the name # <<< Filtering out unwanted networks
lat = device['lat']
lng = device['lng']
# Find the corresponding status for the device
for status in statuses:
if status['name'] == name:
device_status = status['status']
break
else:
device_status = None
# Store the device status with location
devices_with_location[name] = {
'lat': lat,
'lng': lng,
'status': device_status
}
print("Write getOrganizationDeviceStatuses_with_location.json")
# Write the result to a new JSON file
with open('json_files/getOrganizationDeviceStatuses_with_location.json', 'w') as file: # <<<<<< This is the final JSON the webpage calls
json.dump(devices_with_location, file)
===========
Webpage to display the map - It's a bit messy and needs tidying up as it's my first draft but does the job for now. Sometimes Meraki comes back with a device status of 'null' which is strange and I need to work on hadling that better
===========
<!DOCTYPE html>
<html>
<head>
<title>Full Screen Map</title>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
}
#map {
position: absolute;
top: 0;
left: 12%; /* Adjusted the left position to accommodate the marker names */
width: 88%;
height: 100%;
}
#marker-names {
position: absolute;
top: 0;
left: 0;
width: 20%;
height: 100%;
background-color: rgb(10, 15, 22);
color: white;
overflow-y: auto;
}
#marker-names p {
margin: 0;
padding: 5px 0;
}
#problem-devices {
font-weight: bold;
margin-bottom: 10px;
}
.circle {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 5px;
}
#refresh-timer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-family: "Roboto", sans-serif;
font-size: 50px;
}
#timer-svg {
width: 150px;
height: 150px;
}
#timer-text {
font-size: 48px;
fill: white;
text-anchor: middle;
font-family: "Roboto", sans-serif;
font-weight: bold;
letter-spacing: 2px;
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.css" />
</head>
<body>
<div id="map"></div>
<div id="marker-names">
<div id="problem-devices" style="font-size: 20px;">Refreshing data...</div>
<svg id="timer-svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle id="timer-bg" cx="100" cy="100" r="90" fill="none" stroke="rgba(255, 255, 255, 0.1)" stroke-width="20" />
<circle id="timer-progress" cx="100" cy="100" r="90" fill="none" stroke="green" stroke-width="20" stroke-dasharray="565.48" stroke-dashoffset="0" />
<text id="timer-text" x="100" y="100">60</text>
</svg>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
<script>
var map = L.map('map').setView([-30.5, 145], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {}).addTo(map);
map.on('zoomend', function() {
if (map.getZoom() === 4) {
map.setZoom(4.5);
}
});
function refreshPage() {
location.reload();
}
function startRefreshTimer() {
var count = 60; // Refresh countdown in seconds
var startTime = null;
var frameRate = 5; // Desired frame rate (5 frames per second)
var timerText = document.getElementById('timer-text');
var timerProgress = document.getElementById('timer-progress');
function animateTimer(timestamp) {
if (!startTime) startTime = timestamp;
var elapsedTime = timestamp - startTime;
count = Math.max(60 - Math.floor(elapsedTime / 1000), 0);
timerText.textContent = count;
// Update the progress circle
var circumference = 2 * Math.PI * 90;
var dashOffset = circumference - (count / 60) * circumference;
timerProgress.style.strokeDashoffset = dashOffset;
if (count > 0) {
var timePerFrame = 1000 / frameRate;
var timeSinceLastFrame = elapsedTime % timePerFrame;
if (timeSinceLastFrame >= timePerFrame - 1 || timeSinceLastFrame === 0) {
requestAnimationFrame(animateTimer);
} else {
setTimeout(function() {
requestAnimationFrame(animateTimer);
}, timePerFrame - timeSinceLastFrame);
}
} else {
timerText.textContent = '0';
refreshPage();
}
}
requestAnimationFrame(animateTimer);
}
fetch('http://<server IP>/shared_data/getOrganizationDeviceStatuses_with_location.json') # Your server IP
.then(response => response.json())
.then(data => {
var redMarkers = [];
var offlineMarkerNames = [];
var alertingMarkerNames = [];
Object.entries(data).forEach(([key, value]) => {
if (value.status === 'null') {
return; // Ignore when status is 'null'
}
var markerColor;
var markerRadius = 3;
if (value.status === 'online') {
markerColor = 'rgb(0, 200, 0)'; // Slightly brighter green color
} else if (value.status === 'alerting') {
markerColor = 'yellow';
markerRadius *= 2.5;
alertingMarkerNames.push(key); // Add to alerting marker names
} else if (value.status === 'dormant') {
markerColor = 'white';
markerRadius *= 2;
} else {
markerColor = 'red';
markerRadius *= 2.5;
offlineMarkerNames.push(key); // Add to offline marker names
}
if (value.status !== 'null') {
var marker = L.circleMarker([value.lat, value.lng], { radius: markerRadius, color: markerColor });
marker.bindPopup(`<b>${key}</b><br>Status: ${value.status}`);
map.addLayer(marker);
if (markerColor === 'red' || markerColor === 'yellow') {
redMarkers.push(marker);
}
}
});
redMarkers.forEach(marker => {
marker.bringToFront();
});
var flashingMarkers = redMarkers.filter(marker => marker.options.color === 'yellow' || marker.options.color === 'red');
var pulseInterval = setInterval(function() {
flashingMarkers.forEach(marker => {
var radius = marker.options.radius;
var targetRadius1, targetRadius2, targetRadius3, targetRadius4;
if (marker.options.color === 'yellow') {
targetRadius1 = radius * 1; // Increase the radius by 5% for yellow markers
targetRadius2 = radius * 0.1; // Increase the radius by 10% for yellow markers
targetRadius3 = radius * 0.25; // Increase the radius by 15% for yellow markers
targetRadius4 = radius * 0.5; // Reduce the radius by 10% for yellow markers
} else {
targetRadius1 = radius * 1; // Increase the radius by 5% for red markers
targetRadius2 = radius * 0.1; // Increase the radius by 10% for red markers
targetRadius3 = radius * 0.25; // Increase the radius by 15% for red markers
targetRadius4 = radius * 0.5; // Reduce the radius by 10% for red markers
}
marker.setStyle({ radius: targetRadius1 });
setTimeout(function() {
marker.setStyle({ radius: targetRadius2 });
setTimeout(function() {
marker.setStyle({ radius: targetRadius3 });
setTimeout(function() {
marker.setStyle({ radius: targetRadius4 });
setTimeout(function() {
marker.setStyle({ radius: radius }); // Reset the radius to its original value
}, 200); // Wait for 400 milliseconds before resetting the radius
}, 200); // Wait for 400 milliseconds before reducing the radius
}, 200); // Wait for 400 milliseconds before increasing the radius
}, 250); // Wait for 500 milliseconds before increasing the radius
});
}, 1000); // Repeat the pulse animation every 2000 milliseconds (2 seconds)
var markerNamesElement = document.getElementById('marker-names');
var offlineNamesHTML = offlineMarkerNames.map(name => `<p style="color: white; font-family: 'Arial', sans-serif; font-size: 14px;"><span class="circle" style="background-color: red;"></span>${name}</p>`).join('');
var alertingNamesHTML = alertingMarkerNames.map(name => `<p style="color: white; font-family: 'Arial', sans-serif; font-size: 14px;"><span class="circle" style="background-color: yellow;"></span>${name}</p>`).join('');
var problemDevicesElement = document.getElementById('problem-devices');
problemDevicesElement.innerHTML = "<table style='font-family: Roboto, sans-serif;'>" +
"<tr><td><span style='font-size: smaller; color: white;'>Dormant</span> <span style='display: inline-block; width: 12px; height: 12px; background-color: white; border-radius: 50%;'></span></td><td><span style='font-size: smaller; color: white;'>Alerting</span> <span style='display: inline-block; width: 12px; height: 12px; background-color: yellow; border-radius: 50%;'></span></td></tr>" +
"<tr><td><span style='font-size: smaller; color: white;'>Online</span> <span style='display: inline-block; width: 12px; height: 12px; background-color: green; border-radius: 50%;'></span></td><td><span style='font-size: smaller; color: white;'>Offline</span> <span style='display: inline-block; width: 12px; height: 12px; background-color: red; border-radius: 50%;'></span></td></tr>" +
"</table>" +
"<hr>";
markerNamesElement.innerHTML += alertingNamesHTML;
markerNamesElement.innerHTML += offlineNamesHTML;
// Start the refresh timer
startRefreshTimer();
})
.catch(error => console.error('Error:', error));
</script>
</body>
</html>