from microdot import Microdot from microdot.session import with_session from models.preset import Preset from models.profile import Profile from models.transport import get_current_sender from util.espnow_message import build_message, build_preset_dict import asyncio import json controller = Microdot() presets = Preset() profiles = Profile() def get_current_profile_id(session=None): """Get the current active profile ID from session or fallback to first.""" profile_list = profiles.list() session_profile = None if session is not None: session_profile = session.get('current_profile') if session_profile and session_profile in profile_list: return session_profile if profile_list: return profile_list[0] return None @controller.get('') @with_session async def list_presets(request, session): """List presets for the current profile.""" current_profile_id = get_current_profile_id(session) if not current_profile_id: return json.dumps({}), 200, {'Content-Type': 'application/json'} scoped = { pid: pdata for pid, pdata in presets.items() if isinstance(pdata, dict) and str(pdata.get("profile_id")) == str(current_profile_id) } return json.dumps(scoped), 200, {'Content-Type': 'application/json'} @controller.get('/') @with_session async def get_preset(request, session, preset_id): """Get a specific preset by ID (current profile only).""" preset = presets.read(preset_id) current_profile_id = get_current_profile_id(session) if preset and str(preset.get("profile_id")) == str(current_profile_id): return json.dumps(preset), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Preset not found"}), 404 @controller.post('') @with_session async def create_preset(request, session): """Create a new preset for the current profile.""" try: try: data = request.json or {} except Exception: return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'} current_profile_id = get_current_profile_id(session) if not current_profile_id: return json.dumps({"error": "No profile available"}), 404 preset_id = presets.create(current_profile_id) if not isinstance(data, dict): data = {} data = dict(data) data["profile_id"] = str(current_profile_id) if presets.update(preset_id, data): preset_data = presets.read(preset_id) return json.dumps({preset_id: preset_data}), 201, {'Content-Type': 'application/json'} return json.dumps({"error": "Failed to create preset"}), 400 except Exception as e: return json.dumps({"error": str(e)}), 400 @controller.put('/') @with_session async def update_preset(request, session, preset_id): """Update an existing preset (current profile only).""" try: preset = presets.read(preset_id) current_profile_id = get_current_profile_id(session) if not preset or str(preset.get("profile_id")) != str(current_profile_id): return json.dumps({"error": "Preset not found"}), 404 try: data = request.json or {} except Exception: return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'} if not isinstance(data, dict): data = {} data = dict(data) data["profile_id"] = str(current_profile_id) if presets.update(preset_id, data): return json.dumps(presets.read(preset_id)), 200, {'Content-Type': 'application/json'} return json.dumps({"error": "Preset not found"}), 404 except Exception as e: return json.dumps({"error": str(e)}), 400 @controller.delete('/') @with_session async def delete_preset(request, *args, **kwargs): """Delete a preset (current profile only).""" # Be tolerant of wrapper/arg-order variations. session = None preset_id = None if len(args) > 0: session = args[0] if len(args) > 1: preset_id = args[1] if 'session' in kwargs and kwargs.get('session') is not None: session = kwargs.get('session') if 'preset_id' in kwargs and kwargs.get('preset_id') is not None: preset_id = kwargs.get('preset_id') if 'id' in kwargs and kwargs.get('id') is not None and preset_id is None: preset_id = kwargs.get('id') if preset_id is None: return json.dumps({"error": "Preset ID is required"}), 400 preset = presets.read(preset_id) current_profile_id = get_current_profile_id(session) if not preset or str(preset.get("profile_id")) != str(current_profile_id): return json.dumps({"error": "Preset not found"}), 404 if presets.delete(preset_id): return json.dumps({"message": "Preset deleted successfully"}), 200 return json.dumps({"error": "Preset not found"}), 404 @controller.post('/send') @with_session async def send_presets(request, session): """ Send one or more presets to the LED driver (via serial transport). Body JSON: {"preset_ids": ["1", "2", ...]} or {"ids": ["1", "2", ...]} The controller looks up each preset, converts to API format, chunks into <= 240-byte messages, and sends them over the configured transport. """ try: data = request.json or {} except Exception: return json.dumps({"error": "Invalid JSON"}), 400, {'Content-Type': 'application/json'} preset_ids = data.get('preset_ids') or data.get('ids') if not isinstance(preset_ids, list) or not preset_ids: return json.dumps({"error": "preset_ids must be a non-empty list"}), 400, {'Content-Type': 'application/json'} save_flag = data.get('save', True) save_flag = bool(save_flag) default_id = data.get('default') # Optional 12-char hex MAC to send to one device; omit for default (e.g. broadcast). destination_mac = data.get('destination_mac') or data.get('to') # Build API-compliant preset map keyed by preset ID, include name current_profile_id = get_current_profile_id(session) presets_by_name = {} for pid in preset_ids: preset_data = presets.read(str(pid)) if not preset_data: continue if str(preset_data.get("profile_id")) != str(current_profile_id): continue preset_key = str(pid) preset_payload = build_preset_dict(preset_data) preset_payload["name"] = preset_data.get("name", "") presets_by_name[preset_key] = preset_payload if not presets_by_name: return json.dumps({"error": "No matching presets found"}), 404, {'Content-Type': 'application/json'} if default_id is not None and str(default_id) not in presets_by_name: default_id = None sender = get_current_sender() if not sender: return json.dumps({"error": "Transport not configured"}), 503, {'Content-Type': 'application/json'} async def send_chunk(chunk_presets): # Include save flag so the led-driver can persist when desired. msg = build_message(presets=chunk_presets, save=save_flag, default=default_id) await sender.send(msg, addr=destination_mac) MAX_BYTES = 240 send_delay_s = 0.1 entries = list(presets_by_name.items()) total_presets = len(entries) messages_sent = 0 batch = {} last_msg = None for name, preset_obj in entries: test_batch = dict(batch) test_batch[name] = preset_obj test_msg = build_message(presets=test_batch, save=save_flag, default=default_id) size = len(test_msg) if size <= MAX_BYTES or not batch: batch = test_batch last_msg = test_msg else: try: await send_chunk(batch) except Exception: return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} await asyncio.sleep(send_delay_s) messages_sent += 1 batch = {name: preset_obj} last_msg = build_message(presets=batch, save=save_flag, default=default_id) if batch: try: await send_chunk(batch) except Exception: return json.dumps({"error": "Send failed"}), 503, {'Content-Type': 'application/json'} await asyncio.sleep(send_delay_s) messages_sent += 1 return json.dumps({ "message": "Presets sent", "presets_sent": total_presets, "messages_sent": messages_sent }), 200, {'Content-Type': 'application/json'}