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
|
from functools import lru_cache
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@ -15,6 +18,12 @@ last_api_call = {}
|
|||||||
cached_data = {}
|
cached_data = {}
|
||||||
executor = ThreadPoolExecutor(max_workers=3) # Voor parallelle API calls
|
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):
|
def get_cached_data(key, fetch_func, duration=CACHE_DURATION):
|
||||||
"""Haal data uit cache of voer fetch_func uit als cache verlopen is"""
|
"""Haal data uit cache of voer fetch_func uit als cache verlopen is"""
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
@ -28,14 +37,67 @@ def get_cached_data(key, fetch_func, duration=CACHE_DURATION):
|
|||||||
last_api_call[key] = current_time
|
last_api_call[key] = current_time
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def fetch_data_parallel():
|
async def fetch_weather_data_async():
|
||||||
"""Haal alle data parallel op"""
|
"""Asynchrone versie van weather data ophalen"""
|
||||||
futures = {
|
try:
|
||||||
'weather': executor.submit(fetch_weather_data),
|
async with aiohttp.ClientSession() as session:
|
||||||
'sonos': executor.submit(fetch_sonos_zones),
|
params = {
|
||||||
'date': executor.submit(get_date_info)
|
'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
|
# Voeg cache-control headers toe voor statische bestanden
|
||||||
@app.after_request
|
@app.after_request
|
||||||
@ -332,7 +394,8 @@ def index():
|
|||||||
date_info=date_info)
|
date_info=date_info)
|
||||||
|
|
||||||
@app.route('/instellingen', methods=['GET', 'POST'])
|
@app.route('/instellingen', methods=['GET', 'POST'])
|
||||||
def instellingen():
|
@async_route
|
||||||
|
async def instellingen():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
# Nieuwe volume instellingen
|
# Nieuwe volume instellingen
|
||||||
@ -410,8 +473,8 @@ def instellingen():
|
|||||||
# Probeer alsnog door te gaan zonder de Pi volume instelling
|
# Probeer alsnog door te gaan zonder de Pi volume instelling
|
||||||
return redirect('/instellingen')
|
return redirect('/instellingen')
|
||||||
|
|
||||||
# Haal alle data parallel op
|
# Haal alle data parallel op met asyncio
|
||||||
data = fetch_data_parallel()
|
data = await fetch_data_parallel_async()
|
||||||
|
|
||||||
return render_template('settings.html',
|
return render_template('settings.html',
|
||||||
settings=load_settings(),
|
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
|
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: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: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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function previewAudio() {
|
// Debounce functie voor betere performance
|
||||||
const select = document.getElementById('audioSelect');
|
function debounce(func, wait) {
|
||||||
const audio = document.getElementById('previewAudio');
|
let timeout;
|
||||||
const selectedClip = select.value;
|
return function executedFunction(...args) {
|
||||||
|
const later = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
func(...args);
|
||||||
|
};
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
audio.src = `/static/clips/${selectedClip}`;
|
// Cache DOM elementen
|
||||||
audio.play().catch(e => {
|
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);
|
console.log('Audio afspelen mislukt:', e);
|
||||||
alert('Kon audio niet afspelen. Controleer of het bestand bestaat.');
|
alert('Kon audio niet afspelen. Controleer of het bestand bestaat.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-update preview audio source when selection changes
|
// Gebruik event delegation voor betere performance
|
||||||
document.getElementById('audioSelect').addEventListener('change', function() {
|
document.addEventListener('change', function(e) {
|
||||||
const audio = document.getElementById('previewAudio');
|
if (e.target === audioSelect && previewAudio) {
|
||||||
audio.src = `/static/clips/${this.value}`;
|
previewAudio.src = `/static/clips/${e.target.value}`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set initial audio source
|
// Optimize volume updates
|
||||||
window.onload = function() {
|
const updateVolumeDisplay = debounce(function(value) {
|
||||||
const select = document.getElementById('audioSelect');
|
if (volumeDisplay) {
|
||||||
const audio = document.getElementById('previewAudio');
|
volumeDisplay.textContent = value + '%';
|
||||||
audio.src = `/static/clips/${select.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();
|
setupThemeToggle();
|
||||||
updateThemeIcon();
|
updateThemeIcon();
|
||||||
|
|
||||||
// Setup tabs
|
|
||||||
setupTabs();
|
setupTabs();
|
||||||
};
|
});
|
||||||
|
|
||||||
// Theme toggle functionaliteit (gekopieerd van index.html)
|
// Theme toggle functionaliteit (gekopieerd van index.html)
|
||||||
function setupThemeToggle() {
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user