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:
parent
21a79147b2
commit
a5e27baa86
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user