feat(webapp): implementatie van async/await voor API-calls en UI optimalisaties

Deze commit introduceert de overstap van traditionele threading naar async/await voor efficiëntere API-oproepen binnen de adhan-webapp. Dit omvat de vervanging van parallele executors met asyncio, waardoor de webapplicatie asynchroon weer- en Sonos-zonegegevens kan ophalen. Daarnaast zijn er prestatieverbeteringen in de gebruikersinterface doorgevoerd, zoals het cachen van DOM-elementen, debouncing van volume-updates en geoptimaliseerde tab-navigatie. Deze wijzigingen verbeteren de algehele snelheid en responsiviteit van de applicatie aanzienlijk.
This commit is contained in:
filoor 2025-05-29 21:30:40 +02:00
parent 21a79147b2
commit a5e27baa86
3 changed files with 190 additions and 108 deletions

View File

@ -6,6 +6,9 @@ from hijridate import Gregorian
from functools import lru_cache
import time
from concurrent.futures import ThreadPoolExecutor
import asyncio
import aiohttp
from functools import wraps
app = Flask(__name__)
@ -15,6 +18,12 @@ last_api_call = {}
cached_data = {}
executor = ThreadPoolExecutor(max_workers=3) # Voor parallelle API calls
def async_route(f):
@wraps(f)
def wrapped(*args, **kwargs):
return asyncio.run(f(*args, **kwargs))
return wrapped
def get_cached_data(key, fetch_func, duration=CACHE_DURATION):
"""Haal data uit cache of voer fetch_func uit als cache verlopen is"""
current_time = time.time()
@ -28,14 +37,67 @@ def get_cached_data(key, fetch_func, duration=CACHE_DURATION):
last_api_call[key] = current_time
return data
def fetch_data_parallel():
"""Haal alle data parallel op"""
futures = {
'weather': executor.submit(fetch_weather_data),
'sonos': executor.submit(fetch_sonos_zones),
'date': executor.submit(get_date_info)
async def fetch_weather_data_async():
"""Asynchrone versie van weather data ophalen"""
try:
async with aiohttp.ClientSession() as session:
params = {
'q': WEATHER_LOCATION,
'appid': OPENWEATHER_API_KEY,
'units': 'metric',
'lang': 'nl'
}
async with session.get('https://api.openweathermap.org/data/2.5/weather', params=params, timeout=5) as response:
data = await response.json()
weather_info = {
'temperature': round(data['main']['temp']),
'feels_like': round(data['main']['feels_like']),
'description': data['weather'][0]['description'].capitalize(),
'humidity': data['main']['humidity'],
'wind_speed': round(data['wind']['speed'] * 3.6),
'icon': data['weather'][0]['icon']
}
return weather_info
except Exception as e:
print(f"⚠️ Fout bij ophalen weerdata: {e}")
return {
'temperature': '--',
'feels_like': '--',
'description': 'Weer niet beschikbaar',
'humidity': '--',
'wind_speed': '--',
'icon': '01d'
}
async def fetch_sonos_zones_async():
"""Asynchrone versie van Sonos zones ophalen"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(f'http://{SONOS_API_IP}:5005/zones', timeout=5) as response:
data = await response.json()
zones = []
for group in data:
for player in group['members']:
zones.append(player['roomName'])
return sorted(set(zones))
except Exception as e:
print(f'Fout bij ophalen Sonos-zones: {e}')
return ['Woonkamer', 'Slaapkamer', 'Keuken']
async def fetch_data_parallel_async():
"""Haal alle data parallel op met asyncio"""
weather_task = asyncio.create_task(fetch_weather_data_async())
sonos_task = asyncio.create_task(fetch_sonos_zones_async())
date_task = asyncio.create_task(asyncio.to_thread(get_date_info))
weather, sonos, date = await asyncio.gather(weather_task, sonos_task, date_task)
return {
'weather': weather,
'sonos': sonos,
'date': date
}
return {key: future.result() for key, future in futures.items()}
# Voeg cache-control headers toe voor statische bestanden
@app.after_request
@ -332,7 +394,8 @@ def index():
date_info=date_info)
@app.route('/instellingen', methods=['GET', 'POST'])
def instellingen():
@async_route
async def instellingen():
if request.method == 'POST':
try:
# Nieuwe volume instellingen
@ -410,8 +473,8 @@ def instellingen():
# Probeer alsnog door te gaan zonder de Pi volume instelling
return redirect('/instellingen')
# Haal alle data parallel op
data = fetch_data_parallel()
# Haal alle data parallel op met asyncio
data = await fetch_data_parallel_async()
return render_template('settings.html',
settings=load_settings(),

View File

@ -15,3 +15,4 @@ Mon May 26 18:17:57 CEST 2025: Tijdzone probleem opgelost - Container gebruikt n
Wed May 28 14:09:12 CEST 2025: Sonos debug tijd synchronisatie geïmplementeerd - get_current_volume functie en cron script gebruiken nu debug tijd API, volume bepaling werkt correct in debug mode
2025-05-29 21:24:37 - Performance optimalisaties toegevoegd: caching voor API calls en instellingen
2025-05-29 21:27:02 - UI optimalisaties toegevoegd: lazy loading, parallelle API calls en caching
2025-05-29 21:30:24 - Geavanceerde optimalisaties toegevoegd: async/await, debouncing en DOM caching

View File

@ -327,37 +327,134 @@
</div>
<script>
function previewAudio() {
const select = document.getElementById('audioSelect');
const audio = document.getElementById('previewAudio');
const selectedClip = select.value;
// Debounce functie voor betere performance
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
audio.src = `/static/clips/${selectedClip}`;
audio.play().catch(e => {
// Cache DOM elementen
const audioSelect = document.getElementById('audioSelect');
const previewAudio = document.getElementById('previewAudio');
const piVolumeSlider = document.getElementById('piVolumeSlider');
const volumeDisplay = document.getElementById('volumeDisplay');
function previewAudio() {
if (!previewAudio || !audioSelect) return;
const selectedClip = audioSelect.value;
previewAudio.src = `/static/clips/${selectedClip}`;
previewAudio.play().catch(e => {
console.log('Audio afspelen mislukt:', e);
alert('Kon audio niet afspelen. Controleer of het bestand bestaat.');
});
}
// Auto-update preview audio source when selection changes
document.getElementById('audioSelect').addEventListener('change', function() {
const audio = document.getElementById('previewAudio');
audio.src = `/static/clips/${this.value}`;
// Gebruik event delegation voor betere performance
document.addEventListener('change', function(e) {
if (e.target === audioSelect && previewAudio) {
previewAudio.src = `/static/clips/${e.target.value}`;
}
});
// Set initial audio source
window.onload = function() {
const select = document.getElementById('audioSelect');
const audio = document.getElementById('previewAudio');
audio.src = `/static/clips/${select.value}`;
// Optimize volume updates
const updateVolumeDisplay = debounce(function(value) {
if (volumeDisplay) {
volumeDisplay.textContent = value + '%';
}
}, 100);
// Optimize Pi volume testing
const testPiVolume = debounce(function() {
if (!piVolumeSlider) return;
const volume = piVolumeSlider.value;
fetch('/api/set-pi-volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: parseInt(volume) })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Pi volume ingesteld op', volume + '%');
if (previewAudio && previewAudio.src) {
previewAudio.play().catch(e => {
console.log('Audio test mislukt:', e);
alert('Volume ingesteld, maar audio test mislukt. Controleer HDMI verbinding.');
});
} else {
alert(`Pi HDMI volume ingesteld op ${volume}%`);
}
} else {
alert('Fout bij instellen volume: ' + data.error);
}
})
.catch(error => {
console.error('Netwerk fout:', error);
alert('Kon volume niet instellen. Controleer verbinding.');
});
}, 300);
// Optimize tab switching
function setupTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
// Gebruik event delegation voor tab clicks
document.querySelector('.tab-navigation').addEventListener('click', function(e) {
const btn = e.target.closest('.tab-btn');
if (!btn) return;
const targetTab = btn.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Add active class to clicked button and corresponding content
btn.classList.add('active');
document.getElementById(targetTab + '-tab').classList.add('active');
// Save active tab to localStorage
localStorage.setItem('activeSettingsTab', targetTab);
});
// Restore last active tab
const savedTab = localStorage.getItem('activeSettingsTab');
if (savedTab) {
const savedBtn = document.querySelector(`[data-tab="${savedTab}"]`);
const savedContent = document.getElementById(savedTab + '-tab');
if (savedBtn && savedContent) {
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
savedBtn.classList.add('active');
savedContent.classList.add('active');
}
}
}
// Initialize on load
window.addEventListener('load', function() {
if (audioSelect && previewAudio) {
previewAudio.src = `/static/clips/${audioSelect.value}`;
}
// Setup theme toggle
setupThemeToggle();
updateThemeIcon();
// Setup tabs
setupTabs();
};
});
// Theme toggle functionaliteit (gekopieerd van index.html)
function setupThemeToggle() {
@ -387,85 +484,6 @@
}
}
}
// Tab functionality
function setupTabs() {
const tabBtns = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Add active class to clicked button and corresponding content
btn.classList.add('active');
document.getElementById(targetTab + '-tab').classList.add('active');
// Save active tab to localStorage
localStorage.setItem('activeSettingsTab', targetTab);
});
});
// Restore last active tab
const savedTab = localStorage.getItem('activeSettingsTab');
if (savedTab) {
const savedBtn = document.querySelector(`[data-tab="${savedTab}"]`);
const savedContent = document.getElementById(savedTab + '-tab');
if (savedBtn && savedContent) {
// Remove active from all
tabBtns.forEach(b => b.classList.remove('active'));
tabContents.forEach(c => c.classList.remove('active'));
// Activate saved tab
savedBtn.classList.add('active');
savedContent.classList.add('active');
}
}
}
// Pi HDMI Volume functies
function updateVolumeDisplay(value) {
document.getElementById('volumeDisplay').textContent = value + '%';
}
function testPiVolume() {
const volume = document.getElementById('piVolumeSlider').value;
// Verstuur volume naar backend
fetch('/api/set-pi-volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: parseInt(volume) })
})
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Pi volume ingesteld op', volume + '%');
// Test audio afspelen
const audio = document.getElementById('previewAudio');
if (audio && audio.src) {
audio.play().catch(e => {
console.log('Audio test mislukt:', e);
alert('Volume ingesteld, maar audio test mislukt. Controleer HDMI verbinding.');
});
} else {
alert(`Pi HDMI volume ingesteld op ${volume}%`);
}
} else {
alert('Fout bij instellen volume: ' + data.error);
}
})
.catch(error => {
console.error('Netwerk fout:', error);
alert('Kon volume niet instellen. Controleer verbinding.');
});
}
</script>
</body>
</html>