lifx-emulator 3.1.0__py3-none-any.whl → 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {lifx_emulator-3.1.0.dist-info → lifx_emulator-4.2.0.dist-info}/METADATA +2 -1
  2. lifx_emulator-4.2.0.dist-info/RECORD +43 -0
  3. lifx_emulator_app/__main__.py +693 -137
  4. lifx_emulator_app/api/__init__.py +0 -4
  5. lifx_emulator_app/api/app.py +122 -16
  6. lifx_emulator_app/api/models.py +32 -1
  7. lifx_emulator_app/api/routers/__init__.py +5 -1
  8. lifx_emulator_app/api/routers/devices.py +64 -10
  9. lifx_emulator_app/api/routers/products.py +42 -0
  10. lifx_emulator_app/api/routers/scenarios.py +55 -52
  11. lifx_emulator_app/api/routers/websocket.py +70 -0
  12. lifx_emulator_app/api/services/__init__.py +21 -4
  13. lifx_emulator_app/api/services/device_service.py +188 -1
  14. lifx_emulator_app/api/services/event_bridge.py +234 -0
  15. lifx_emulator_app/api/services/scenario_service.py +153 -0
  16. lifx_emulator_app/api/services/websocket_manager.py +326 -0
  17. lifx_emulator_app/api/static/_app/env.js +1 -0
  18. lifx_emulator_app/api/static/_app/immutable/assets/0.DOQLX7EM.css +1 -0
  19. lifx_emulator_app/api/static/_app/immutable/assets/2.CU0O2Xrb.css +1 -0
  20. lifx_emulator_app/api/static/_app/immutable/chunks/BORyfda6.js +1 -0
  21. lifx_emulator_app/api/static/_app/immutable/chunks/BTLkiQR5.js +1 -0
  22. lifx_emulator_app/api/static/_app/immutable/chunks/BaoxLdOF.js +2 -0
  23. lifx_emulator_app/api/static/_app/immutable/chunks/Binc8JbE.js +1 -0
  24. lifx_emulator_app/api/static/_app/immutable/chunks/CDSQEL5N.js +1 -0
  25. lifx_emulator_app/api/static/_app/immutable/chunks/DfIkQq0Y.js +1 -0
  26. lifx_emulator_app/api/static/_app/immutable/chunks/MAGDeS2Z.js +1 -0
  27. lifx_emulator_app/api/static/_app/immutable/chunks/N3z8axFy.js +1 -0
  28. lifx_emulator_app/api/static/_app/immutable/chunks/yhjkpkcN.js +1 -0
  29. lifx_emulator_app/api/static/_app/immutable/entry/app.Dhwm664s.js +2 -0
  30. lifx_emulator_app/api/static/_app/immutable/entry/start.Nqz6UJJT.js +1 -0
  31. lifx_emulator_app/api/static/_app/immutable/nodes/0.CPncm6RP.js +1 -0
  32. lifx_emulator_app/api/static/_app/immutable/nodes/1.x-f3libw.js +1 -0
  33. lifx_emulator_app/api/static/_app/immutable/nodes/2.BP5Yvqf4.js +6 -0
  34. lifx_emulator_app/api/static/_app/version.json +1 -0
  35. lifx_emulator_app/api/static/index.html +38 -0
  36. lifx_emulator_app/api/static/robots.txt +3 -0
  37. lifx_emulator_app/config.py +316 -0
  38. lifx_emulator-3.1.0.dist-info/RECORD +0 -19
  39. lifx_emulator_app/api/static/dashboard.js +0 -588
  40. lifx_emulator_app/api/templates/dashboard.html +0 -357
  41. {lifx_emulator-3.1.0.dist-info → lifx_emulator-4.2.0.dist-info}/WHEEL +0 -0
  42. {lifx_emulator-3.1.0.dist-info → lifx_emulator-4.2.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,6 @@
1
+ import{a as h,f as k,c as Le}from"../chunks/Binc8JbE.js";import{i as kt}from"../chunks/BORyfda6.js";import{o as Xt,a as Kt}from"../chunks/yhjkpkcN.js";import{i as _t,b as Qt,h as we,c as pt,L as $t,a as ea,w as a,r as ta,H as aa,s as At,d as bt,k as ct,S as ra,as as sa,ab as Pt,e as Me,aN as Ne,j as yt,l as ia,aH as na,aO as Rt,aq as St,aP as la,aQ as oa,aL as ca,Y as Tt,aR as va,f as Ot,p as Bt,aS as mt,aT as da,ap as ua,g as fa,U as _a,aU as Ht,_ as Vt,aV as pa,aj as ba,aW as ma,aX as ha,aY as Et,V as ga,aZ as Ut,a_ as wa,a$ as ya,b0 as xa,b1 as ka,b2 as Sa,aM as Ea,q as Wt,X as jt,aw as f,au as J,B as Re,C as de,D as B,F as Oe,G as i,J as o,I as s,ac as vt,av as lt,ax as De,o as dt}from"../chunks/DfIkQq0Y.js";import{d as qe,s as F,e as Fa}from"../chunks/BaoxLdOF.js";import{i as R}from"../chunks/BTLkiQR5.js";import{s as Ye,a as ot,d as V,b as je,u as I,t as ht}from"../chunks/CDSQEL5N.js";function Ie(e,t){return t}function Ia(e,t,n){for(var r=[],l=t.length,c,v=t.length,m=0;m<l;m++){let g=t[m];Bt(g,()=>{if(c){if(c.pending.delete(g),c.done.add(g),c.pending.size===0){var u=e.outrogroups;xt(St(c.done)),u.delete(c),u.size===0&&(e.outrogroups=null)}}else v-=1},!1)}if(v===0){var _=r.length===0&&n!==null;if(_){var w=n,y=w.parentNode;ua(y),y.append(w),e.items.clear()}xt(t,!_)}else c={pending:new Set(t),done:new Set},(e.outrogroups??=new Set).add(c)}function xt(e,t=!0){for(var n=0;n<e.length;n++)fa(e[n],t)}var zt;function ye(e,t,n,r,l,c=null){var v=e,m=new Map,_=(t&Ht)!==0;if(_){var w=e;v=we?pt($t(w)):w.appendChild(_t())}we&&ea();var y=null,g=na(()=>{var x=n();return Rt(x)?x:x==null?[]:St(x)}),u,p=!0;function A(){b.fallback=y,La(b,u,v,t,r),y!==null&&(u.length===0?(y.f&Ne)===0?Ot(y):(y.f^=Ne,st(y,null,v)):Bt(y,()=>{y=null}))}var X=Qt(()=>{u=a(g);var x=u.length;let C=!1;if(we){var T=ta(v)===aa;T!==(x===0)&&(v=At(),pt(v),bt(!1),C=!0)}for(var M=new Set,L=Me,z=ia(),O=0;O<x;O+=1){we&&ct.nodeType===ra&&ct.data===sa&&(v=ct,C=!0,bt(!1));var W=u[O],j=r(W,O),H=p?null:m.get(j);H?(H.v&&Pt(H.v,W),H.i&&Pt(H.i,O),z&&L.skipped_effects.delete(H.e)):(H=Ca(m,p?v:zt??=_t(),W,j,O,l,t,n),p||(H.e.f|=Ne),m.set(j,H)),M.add(j)}if(x===0&&c&&!y&&(p?y=yt(()=>c(v)):(y=yt(()=>c(zt??=_t())),y.f|=Ne)),we&&x>0&&pt(At()),!p)if(z){for(const[ue,ee]of m)M.has(ue)||L.skipped_effects.add(ee.e);L.oncommit(A),L.ondiscard(()=>{})}else A();C&&bt(!0),a(g)}),b={effect:X,items:m,outrogroups:null,fallback:y};p=!1,we&&(v=ct)}function rt(e){for(;e!==null&&(e.f&da)===0;)e=e.next;return e}function La(e,t,n,r,l){var c=(r&pa)!==0,v=t.length,m=e.items,_=rt(e.effect.first),w,y=null,g,u=[],p=[],A,X,b,x;if(c)for(x=0;x<v;x+=1)A=t[x],X=l(A,x),b=m.get(X).e,(b.f&Ne)===0&&(b.nodes?.a?.measure(),(g??=new Set).add(b));for(x=0;x<v;x+=1){if(A=t[x],X=l(A,x),b=m.get(X).e,e.outrogroups!==null)for(const H of e.outrogroups)H.pending.delete(b),H.done.delete(b);if((b.f&Ne)!==0)if(b.f^=Ne,b===_)st(b,null,n);else{var C=y?y.next:_;b===e.effect.last&&(e.effect.last=b.prev),b.prev&&(b.prev.next=b.next),b.next&&(b.next.prev=b.prev),We(e,y,b),We(e,b,C),st(b,C,n),y=b,u=[],p=[],_=rt(y.next);continue}if((b.f&mt)!==0&&(Ot(b),c&&(b.nodes?.a?.unfix(),(g??=new Set).delete(b))),b!==_){if(w!==void 0&&w.has(b)){if(u.length<p.length){var T=p[0],M;y=T.prev;var L=u[0],z=u[u.length-1];for(M=0;M<u.length;M+=1)st(u[M],T,n);for(M=0;M<p.length;M+=1)w.delete(p[M]);We(e,L.prev,z.next),We(e,y,L),We(e,z,T),_=T,y=z,x-=1,u=[],p=[]}else w.delete(b),st(b,_,n),We(e,b.prev,b.next),We(e,b,y===null?e.effect.first:y.next),We(e,y,b),y=b;continue}for(u=[],p=[];_!==null&&_!==b;)(w??=new Set).add(_),p.push(_),_=rt(_.next);if(_===null)continue}(b.f&Ne)===0&&u.push(b),y=b,_=rt(b.next)}if(e.outrogroups!==null){for(const H of e.outrogroups)H.pending.size===0&&(xt(St(H.done)),e.outrogroups?.delete(H));e.outrogroups.size===0&&(e.outrogroups=null)}if(_!==null||w!==void 0){var O=[];if(w!==void 0)for(b of w)(b.f&mt)===0&&O.push(b);for(;_!==null;)(_.f&mt)===0&&_!==e.fallback&&O.push(_),_=rt(_.next);var W=O.length;if(W>0){var j=(r&Ht)!==0&&v===0?n:null;if(c){for(x=0;x<W;x+=1)O[x].nodes?.a?.measure();for(x=0;x<W;x+=1)O[x].nodes?.a?.fix()}Ia(e,O,j)}}c&&Vt(()=>{if(g!==void 0)for(b of g)b.nodes?.a?.apply()})}function Ca(e,t,n,r,l,c,v,m){var _=(v&la)!==0?(v&oa)===0?ca(n,!1,!1):Tt(n):null,w=(v&va)!==0?Tt(l):null;return{v:_,i:w,e:yt(()=>(c(t,_??n,w??l,m),()=>{e.delete(r)}))}}function st(e,t,n){if(e.nodes)for(var r=e.nodes.start,l=e.nodes.end,c=t&&(t.f&Ne)===0?t.nodes.start:n;r!==null;){var v=_a(r);if(c.before(r),r===l)return;r=v}}function We(e,t,n){t===null?e.effect.first=n:t.next=n,n===null?e.effect.last=t:n.prev=t}function qt(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var l=e.length;for(t=0;t<l;t++)e[t]&&(n=qt(e[t]))&&(r&&(r+=" "),r+=n)}else for(n in e)e[n]&&(r&&(r+=" "),r+=n);return r}function Aa(){for(var e,t,n=0,r="",l=arguments.length;n<l;n++)(e=arguments[n])&&(t=qt(e))&&(r&&(r+=" "),r+=t);return r}function Pa(e){return typeof e=="object"?Aa(e):e??""}const Mt=[...`
2
+ \r\f \v\uFEFF`];function Ta(e,t,n){var r=e==null?"":""+e;if(t&&(r=r?r+" "+t:t),n){for(var l in n)if(n[l])r=r?r+" "+l:l;else if(r.length)for(var c=l.length,v=0;(v=r.indexOf(l,v))>=0;){var m=v+c;(v===0||Mt.includes(r[v-1]))&&(m===r.length||Mt.includes(r[m]))?r=(v===0?"":r.substring(0,v))+r.substring(m+1):v=m}}return r===""?null:r}function za(e,t){return e==null?null:String(e)}function Xe(e,t,n,r,l,c){var v=e.__className;if(we||v!==n||v===void 0){var m=Ta(n,r,c);(!we||m!==e.getAttribute("class"))&&(m==null?e.removeAttribute("class"):e.className=m),e.__className=n}else if(c&&l!==c)for(var _ in c){var w=!!c[_];(l==null||w!==!!l[_])&&e.classList.toggle(_,w)}return c}function Ke(e,t,n,r){var l=e.__style;if(we||l!==t){var c=za(t);(!we||c!==e.getAttribute("style"))&&(c==null?e.removeAttribute("style"):e.style.cssText=c),e.__style=t}return r}function Ft(e,t,n=!1){if(e.multiple){if(t==null)return;if(!Rt(t))return ma();for(var r of e.options)r.selected=t.includes(it(r));return}for(r of e.options){var l=it(r);if(ha(l,t)){r.selected=!0;return}}(!n||t!==void 0)&&(e.selectedIndex=-1)}function Gt(e){var t=new MutationObserver(()=>{Ft(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),ba(()=>{t.disconnect()})}function It(e,t,n=t){var r=new WeakSet,l=!0;Et(e,"change",c=>{var v=c?"[selected]":":checked",m;if(e.multiple)m=[].map.call(e.querySelectorAll(v),it);else{var _=e.querySelector(v)??e.querySelector("option:not([disabled])");m=_&&it(_)}n(m),Me!==null&&r.add(Me)}),ga(()=>{var c=t();if(e===document.activeElement){var v=Ut??Me;if(r.has(v))return}if(Ft(e,c,l),l&&c===void 0){var m=e.querySelector(":checked");m!==null&&(c=it(m),n(c))}e.__value=c,l=!1}),Gt(e)}function it(e){return"__value"in e?e.__value:e.value}const Ma=Symbol("is custom element"),Na=Symbol("is html");function Se(e){if(we){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute("value")){var r=e.value;$e(e,"value",null),e.value=r}if(e.hasAttribute("checked")){var l=e.checked;$e(e,"checked",null),e.checked=l}}};e.__on_r=n,Vt(n),ka()}}function $e(e,t,n,r){var l=Da(e);we&&(l[t]=e.getAttribute(t),t==="src"||t==="srcset"||t==="href"&&e.nodeName==="LINK")||l[t]!==(l[t]=n)&&(t==="loading"&&(e[Sa]=n),n==null?e.removeAttribute(t):typeof n!="string"&&Xa(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function Da(e){return e.__attributes??={[Ma]:e.nodeName.includes("-"),[Na]:e.namespaceURI===wa}}var Nt=new Map;function Xa(e){var t=e.getAttribute("is")||e.nodeName,n=Nt.get(t);if(n)return n;Nt.set(t,n=[]);for(var r,l=e,c=Element.prototype;c!==l;){r=xa(l);for(var v in r)r[v].set&&n.push(v);l=ya(l)}return n}function Fe(e,t,n=t){var r=new WeakSet;Et(e,"input",async l=>{var c=l?e.defaultValue:e.value;if(c=gt(e)?wt(c):c,n(c),Me!==null&&r.add(Me),await Ea(),c!==(c=t())){var v=e.selectionStart,m=e.selectionEnd,_=e.value.length;if(e.value=c??"",m!==null){var w=e.value.length;v===m&&m===_&&w>_?(e.selectionStart=w,e.selectionEnd=w):(e.selectionStart=v,e.selectionEnd=Math.min(m,w))}}}),(we&&e.defaultValue!==e.value||Wt(t)==null&&e.value)&&(n(gt(e)?wt(e.value):e.value),Me!==null&&r.add(Me)),jt(()=>{var l=t();if(e===document.activeElement){var c=Ut??Me;if(r.has(c))return}gt(e)&&l===wt(e.value)||e.type==="date"&&!l&&!e.value||l!==e.value&&(e.value=l??"")})}function Ra(e,t,n=t){Et(e,"change",r=>{var l=r?e.defaultChecked:e.checked;n(l)}),(we&&e.defaultChecked!==e.checked||Wt(t)==null)&&n(e.checked),jt(()=>{var r=t();e.checked=!!r})}function gt(e){var t=e.type;return t==="number"||t==="range"}function wt(e){return e===""?null:+e}const Dt=2e3,Oa=3e4;function Ba(){let e=J("disconnected"),t=null,n=Dt,r=null,l=!0;function c(){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`}function v(g){try{const u=JSON.parse(g.data);switch(u.type){case"sync":{const p=u.data;p.stats&&je.set(p.stats),p.devices&&V.set(p.devices),p.activity&&ot.set(p.activity),p.scenarios&&Ye.setAll(p.scenarios);break}case"stats":je.set(u.data);break;case"device_added":V.add(u.data);break;case"device_removed":{const{serial:p}=u.data;V.remove(p);break}case"device_updated":{const{serial:p,changes:A}=u.data;V.update(p,A);break}case"activity":ot.add(u.data);break;case"scenario_changed":Ye.handleChange(u.data);break;case"error":console.error("WebSocket error:",u.message);break}}catch(u){console.error("Failed to parse WebSocket message:",u)}}function m(){if(l=!0,!(t&&(t.readyState===WebSocket.CONNECTING||t.readyState===WebSocket.OPEN))){f(e,"connecting");try{t=new WebSocket(c()),t.onopen=()=>{f(e,"connected"),n=Dt,t.send(JSON.stringify({type:"subscribe",topics:["stats","devices","activity","scenarios"]})),t.send(JSON.stringify({type:"sync"}))},t.onmessage=v,t.onclose=()=>{f(e,"disconnected"),t=null,l&&_()},t.onerror=()=>{f(e,"error"),t?.close()}}catch(g){console.error("WebSocket connection error:",g),f(e,"error"),_()}}}function _(){r&&clearTimeout(r),r=setTimeout(()=>{r=null,m()},n),n=Math.min(n*1.5,Oa)}function w(){l=!1,r&&(clearTimeout(r),r=null),t&&(t.close(),t=null),f(e,"disconnected")}function y(g){t&&t.readyState===WebSocket.OPEN&&t.send(JSON.stringify(g))}return{get status(){return a(e)},connect:m,disconnect:w,send:y,sync(){y({type:"sync"})}}}const Qe=Ba();var Ha=k('<header class="header"><div class="header-title"><h1>LIFX Emulator Monitor</h1> <span></span></div> <div class="header-controls svelte-1elxaub"><button><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="9" width="3" height="6" rx="0.5"></rect><rect x="6" y="5" width="3" height="10" rx="0.5"></rect><rect x="11" y="1" width="3" height="14" rx="0.5"></rect></svg></button> <button class="control-btn svelte-1elxaub"> </button></div></header> <p class="subtitle">Real-time monitoring and device management</p>',1);function Va(e,t){Re(t,!1);const n={light:"☀",dark:"☾",system:"◐"};kt();var r=Ha(),l=de(r),c=i(l),v=o(i(c),2);let m;s(c);var _=o(c,2),w=i(_);let y;w.__click=()=>I.toggleShowStats();var g=o(w,2);g.__click=()=>ht.toggle();var u=i(g,!0);s(g),s(_),s(l),vt(2),B(()=>{m=Xe(v,1,"status-indicator",null,m,{connecting:Qe.status==="connecting",disconnected:Qe.status==="disconnected",error:Qe.status==="error"}),$e(v,"title",Qe.status),y=Xe(w,1,"control-btn svelte-1elxaub",null,y,{active:I.showStats}),$e(w,"title",I.showStats?"Hide statistics":"Show statistics"),$e(g,"title",`Toggle theme (${ht.mode})`),F(u,n[ht.mode])}),h(e,r),Oe()}qe(["click"]);function nt(e){const t=e.hue/65535,n=e.saturation/65535,r=e.brightness/65535;let l,c,v;const m=Math.floor(t*6),_=t*6-m,w=r*(1-n),y=r*(1-_*n),g=r*(1-(1-_)*n);switch(m%6){case 0:l=r,c=g,v=w;break;case 1:l=y,c=r,v=w;break;case 2:l=w,c=r,v=g;break;case 3:l=w,c=y,v=r;break;case 4:l=g,c=w,v=r;break;default:l=r,c=w,v=y;break}const u=Math.round(l*255),p=Math.round(c*255),A=Math.round(v*255);return`rgb(${u}, ${p}, ${A})`}function Ua(e){const t=Math.floor(e);if(t<60)return`${t}s`;if(t<3600)return`${Math.floor(t/60)}m ${t%60}s`;const n=Math.floor(t/3600),r=Math.floor(t%3600/60);return`${n}h ${r}m`}var Wa=k('<div class="card"><h2><span class="status-indicator"></span> Server Statistics</h2> <div class="stats-list"><div class="stat"><span class="stat-label">Uptime</span> <span class="stat-value"> </span></div> <div class="stat"><span class="stat-label">Devices</span> <span class="stat-value"> </span></div> <div class="stat"><span class="stat-label">Packets RX</span> <span class="stat-value"> </span></div> <div class="stat"><span class="stat-label">Packets TX</span> <span class="stat-value"> </span></div> <div class="stat"><span class="stat-label">Errors</span> <span class="stat-value"> </span></div></div></div>');function ja(e,t){Re(t,!1),kt();var n=Wa(),r=o(i(n),2),l=i(r),c=o(i(l),2),v=i(c,!0);s(c),s(l);var m=o(l,2),_=o(i(m),2),w=i(_,!0);s(_),s(m);var y=o(m,2),g=o(i(y),2),u=i(g,!0);s(g),s(y);var p=o(y,2),A=o(i(p),2),X=i(A,!0);s(A),s(p);var b=o(p,2),x=o(i(b),2),C=i(x,!0);s(x),s(b),s(r),s(n),B(T=>{F(v,T),F(w,je.value.device_count),F(u,je.value.packets_received),F(X,je.value.packets_sent),F(C,je.value.error_count)},[()=>Ua(je.value.uptime_seconds)]),h(e,n),Oe()}const Ge="/api";async function qa(){const e=await fetch(`${Ge}/products`);if(!e.ok)throw new Error(`Failed to fetch products: ${e.status}`);return e.json()}async function Ga(e){const t=await fetch(`${Ge}/devices`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({product_id:e})});if(!t.ok){const n=await t.json();throw new Error(n.detail||`Failed to create device: ${t.status}`)}}async function Jt(e){const t=await fetch(`${Ge}/devices/${e}`,{method:"DELETE"});if(!t.ok)throw new Error(`Failed to delete device: ${t.status}`)}async function Lt(){const e=await fetch(`${Ge}/devices`,{method:"DELETE"});if(!e.ok)throw new Error(`Failed to remove devices: ${e.status}`);return e.json()}async function Ja(e){const t=await fetch(`${Ge}/scenarios/global`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok){const n=await t.json();throw new Error(n.detail||`Failed to set global scenario: ${t.status}`)}}async function Ya(){const e=await fetch(`${Ge}/scenarios/global`,{method:"DELETE"});if(!e.ok)throw new Error(`Failed to clear global scenario: ${e.status}`)}async function Za(e,t,n){const r=await fetch(`${Ge}/scenarios/${e}/${encodeURIComponent(t)}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok){const l=await r.json();throw new Error(l.detail||`Failed to set ${e} scenario: ${r.status}`)}}async function Ka(e,t){const n=await fetch(`${Ge}/scenarios/${e}/${encodeURIComponent(t)}`,{method:"DELETE"});if(n.status!==404&&!n.ok)throw new Error(`Failed to clear ${e} scenario: ${n.status}`)}var Qa=k("<option> </option>"),$a=k("<option>Loading products...</option>"),er=k("<option> </option>"),tr=k('<p style="color: var(--accent-danger); font-size: 0.85em; margin-top: 10px;"> </p>'),ar=k('<div class="card"><div class="toolbar-header svelte-zw83d1"><h2>Device Management</h2> <div class="toolbar-actions svelte-zw83d1"><div class="view-toggle svelte-zw83d1"><button title="Card view"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="1" width="6" height="6" rx="1"></rect><rect x="9" y="1" width="6" height="6" rx="1"></rect><rect x="1" y="9" width="6" height="6" rx="1"></rect><rect x="9" y="9" width="6" height="6" rx="1"></rect></svg></button> <button title="Table view"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="2" width="14" height="2" rx="0.5"></rect><rect x="1" y="7" width="14" height="2" rx="0.5"></rect><rect x="1" y="12" width="14" height="2" rx="0.5"></rect></svg></button></div> <div class="page-size svelte-zw83d1"><label for="page-size" class="svelte-zw83d1">Show:</label> <select id="page-size" class="svelte-zw83d1"></select></div> <button class="btn btn-delete"> </button></div></div> <form><div class="form-row"><div class="form-group"><label for="product-id">Product</label> <select id="product-id"><!></select></div> <button type="submit" class="btn"> </button></div> <!></form></div>');function rr(e,t){Re(t,!0);let n=J(lt([])),r=J(null),l=J(!1),c=J(!1),v=J(null);const m=[10,20,50,100];Xt(async()=>{try{f(n,await qa(),!0),a(n).length>0&&f(r,a(n)[0].pid,!0)}catch(D){f(v,D instanceof Error?D.message:"Failed to load products",!0)}});async function _(D){if(D.preventDefault(),a(r)!==null){f(l,!0),f(v,null);try{await Ga(a(r))}catch(G){f(v,G instanceof Error?G.message:"Failed to create device",!0)}finally{f(l,!1)}}}async function w(){if(V.count!==0&&confirm(`Remove all ${V.count} devices?`)){f(c,!0),f(v,null);try{const D=await Lt();console.log(`Removed ${D.removed} devices`)}catch(D){f(v,D instanceof Error?D.message:"Failed to remove devices",!0)}finally{f(c,!1)}}}function y(D){const G=D.target;I.setPageSize(parseInt(G.value,10))}var g=ar(),u=i(g),p=o(i(u),2),A=i(p),X=i(A);let b;X.__click=()=>I.setViewMode("card");var x=o(X,2);let C;x.__click=()=>I.setViewMode("table"),s(A);var T=o(A,2),M=o(i(T),2);M.__change=y,ye(M,21,()=>m,Ie,(D,G)=>{var te=Qa(),$=i(te,!0);s(te);var ce={};B(()=>{F($,a(G)),ce!==(ce=a(G))&&(te.value=(te.__value=a(G))??"")}),h(D,te)}),s(M);var L;Gt(M),s(T);var z=o(T,2);z.__click=w;var O=i(z,!0);s(z),s(p),s(u);var W=o(u,2),j=i(W),H=i(j),ue=o(i(H),2),ee=i(ue);{var P=D=>{var G=$a();G.value=G.__value="",h(D,G)},Q=D=>{var G=Le(),te=de(G);ye(te,17,()=>a(n),$=>$.pid,($,ce)=>{var me=er(),S=i(me);s(me);var N={};B(()=>{F(S,`${a(ce).pid??""} - ${a(ce).name??""}`),N!==(N=a(ce).pid)&&(me.value=(me.__value=a(ce).pid)??"")}),h($,me)}),h(D,G)};R(ee,D=>{a(n).length===0?D(P):D(Q,!1)})}s(ue),s(H);var fe=o(H,2),xe=i(fe,!0);s(fe),s(j);var be=o(j,2);{var ne=D=>{var G=tr(),te=i(G,!0);s(G),B(()=>F(te,a(v))),h(D,G)};R(be,D=>{a(v)&&D(ne)})}s(W),s(g),B(()=>{b=Xe(X,1,"view-btn svelte-zw83d1",null,b,{active:I.viewMode==="card"}),C=Xe(x,1,"view-btn svelte-zw83d1",null,C,{active:I.viewMode==="table"}),L!==(L=I.pageSize)&&(M.value=(M.__value=I.pageSize)??"",Ft(M,I.pageSize)),z.disabled=a(c)||V.count===0,$e(z,"title",V.count===0?"No devices to remove":"Remove all devices"),F(O,a(c)?"Removing...":"Remove All"),ue.disabled=a(n).length===0,fe.disabled=a(l)||a(r)===null,F(xe,a(l)?"Adding...":"Add Device")}),Fa("submit",W,_),It(ue,()=>a(r),D=>f(r,D)),h(e,g),Oe()}qe(["click","change"]);var sr=k('<span class="badge badge-capability">color</span>'),ir=k('<span class="badge badge-capability">IR</span>'),nr=k('<span class="badge badge-extended-mz"> </span>'),lr=k('<span class="badge badge-capability"> </span>'),or=k('<span class="badge badge-capability"> </span>'),cr=k('<span class="badge badge-capability">HEV</span>'),vr=k('<div class="metadata-display"><div class="metadata-row"><span class="metadata-label">Firmware:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">Vendor:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">Product:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">Capabilities:</span> <span class="metadata-value" style="color: var(--accent-primary);"> </span></div> <div class="metadata-row"><span class="metadata-label">Group:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">Location:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">Uptime:</span> <span class="metadata-value"> </span></div> <div class="metadata-row"><span class="metadata-label">WiFi Signal:</span> <span class="metadata-value"> </span></div></div>'),dr=k('<div class="zone-segment"></div>'),ur=k('<div class="zone-strip"></div>'),fr=k('<button type="button" class="toggle-link"> </button> <!>',1),_r=k('<div class="tile-zone"></div>'),pr=k('<div class="tile-grid"></div>'),br=k('<div style="color: var(--text-dimmed);">No color data</div>'),mr=k('<div class="tile-item"><div class="tile-label"></div> <!></div>'),hr=k('<div class="tiles-container"></div>'),gr=k('<button type="button" class="toggle-link"> </button> <!>',1),wr=k('<div style="margin-top: 4px;"><span class="color-swatch"></span> <span style="color: var(--text-muted); font-size: 0.75em;">Current color</span></div>'),yr=k('<div class="device"><div class="device-header"><div><div class="device-serial"> </div> <div class="device-label"> </div></div> <button class="btn btn-delete btn-sm"> </button></div> <div class="badges"><span> </span> <span class="badge badge-capability"> </span> <!> <!> <!> <!> <!></div> <button type="button" class="toggle-link"> </button> <!> <!></div>');function xr(e,t){Re(t,!0);let n=J(!1);async function r(){if(confirm(`Delete device ${t.device.serial}?`)){f(n,!0);try{await Jt(t.device.serial)}catch(S){alert(S instanceof Error?S.message:"Failed to delete device")}finally{f(n,!1)}}}function l(S){return`${Math.floor(S/1e9)}s`}function c(){const S=[];return t.device.has_color&&S.push("Color"),t.device.has_infrared&&S.push("Infrared"),t.device.has_multizone&&S.push(`Multizone (${t.device.zone_count} zones)`),t.device.has_extended_multizone&&S.push("Extended Multizone"),t.device.has_matrix&&S.push(`Matrix (${t.device.tile_count} tiles)`),t.device.has_hev&&S.push("HEV/Clean"),t.device.has_relays&&S.push("Relays"),t.device.has_buttons&&S.push("Buttons"),S.join(", ")||"None"}let v=De(()=>I.isZonesExpanded(t.device.serial)),m=De(()=>I.isMetadataExpanded(t.device.serial));var _=yr(),w=i(_),y=i(w),g=i(y),u=i(g,!0);s(g);var p=o(g,2),A=i(p,!0);s(p),s(y);var X=o(y,2);X.__click=r;var b=i(X,!0);s(X),s(w);var x=o(w,2),C=i(x);let T;var M=i(C,!0);s(C);var L=o(C,2),z=i(L);s(L);var O=o(L,2);{var W=S=>{var N=sr();h(S,N)};R(O,S=>{t.device.has_color&&S(W)})}var j=o(O,2);{var H=S=>{var N=ir();h(S,N)};R(j,S=>{t.device.has_infrared&&S(H)})}var ue=o(j,2);{var ee=S=>{var N=nr(),Z=i(N);s(N),B(()=>F(Z,`extended-mz×${t.device.zone_count??""}`)),h(S,N)},P=S=>{var N=Le(),Z=de(N);{var _e=ve=>{var q=lr(),se=i(q);s(q),B(()=>F(se,`multizone×${t.device.zone_count??""}`)),h(ve,q)};R(Z,ve=>{t.device.has_multizone&&ve(_e)},!0)}h(S,N)};R(ue,S=>{t.device.has_extended_multizone?S(ee):S(P,!1)})}var Q=o(ue,2);{var fe=S=>{var N=or(),Z=i(N);s(N),B(()=>F(Z,`matrix×${t.device.tile_count??""}`)),h(S,N)};R(Q,S=>{t.device.has_matrix&&S(fe)})}var xe=o(Q,2);{var be=S=>{var N=cr();h(S,N)};R(xe,S=>{t.device.has_hev&&S(be)})}s(x);var ne=o(x,2);ne.__click=()=>I.toggleMetadataExpanded(t.device.serial);var D=i(ne);s(ne);var G=o(ne,2);{var te=S=>{var N=vr(),Z=i(N),_e=o(i(Z),2),ve=i(_e);s(_e),s(Z);var q=o(Z,2),se=o(i(q),2),Y=i(se,!0);s(se),s(q);var ae=o(q,2),re=o(i(ae),2),le=i(re,!0);s(re),s(ae);var oe=o(ae,2),K=o(i(oe),2),pe=i(K,!0);s(K),s(oe);var ie=o(oe,2),Pe=o(i(ie),2),Be=i(Pe,!0);s(Pe),s(ie);var He=o(ie,2),Je=o(i(He),2),et=i(Je,!0);s(Je),s(He);var Ve=o(He,2),he=o(i(Ve),2),Ce=i(he,!0);s(he),s(Ve);var Te=o(Ve,2),Ze=o(i(Te),2),d=i(Ze);s(Ze),s(Te),s(N),B((E,U,ge)=>{F(ve,`${t.device.version_major??""}.${t.device.version_minor??""}`),F(Y,t.device.vendor),F(le,t.device.product),F(pe,E),F(Be,t.device.group_label),F(et,t.device.location_label),F(Ce,U),F(d,`${ge??""} dBm`)},[c,()=>l(t.device.uptime_ns),()=>t.device.wifi_signal.toFixed(1)]),h(S,N)};R(G,S=>{a(m)&&S(te)})}var $=o(G,2);{var ce=S=>{var N=fr(),Z=de(N);Z.__click=()=>I.toggleZonesExpanded(t.device.serial);var _e=i(Z);s(Z);var ve=o(Z,2);{var q=se=>{var Y=ur();ye(Y,21,()=>t.device.zone_colors,Ie,(ae,re)=>{var le=dr();B(oe=>Ke(le,`background: ${oe??""};`),[()=>nt(a(re))]),h(ae,le)}),s(Y),h(se,Y)};R(ve,se=>{a(v)&&se(q)})}B(()=>F(_e,`${a(v)?"▾":"▸"} ${a(v)?"Hide":"Show"} zones (${t.device.zone_colors.length??""})`)),h(S,N)},me=S=>{var N=Le(),Z=de(N);{var _e=q=>{var se=gr(),Y=de(se);Y.__click=()=>I.toggleZonesExpanded(t.device.serial);var ae=i(Y);s(Y);var re=o(Y,2);{var le=oe=>{var K=hr();ye(K,21,()=>t.device.tile_devices,Ie,(pe,ie,Pe)=>{var Be=mr(),He=i(Be);He.textContent=`T${Pe+1}`;var Je=o(He,2);{var et=he=>{var Ce=pr();ye(Ce,21,()=>a(ie).colors.slice(0,(a(ie).width||8)*(a(ie).height||8)),Ie,(Te,Ze)=>{var d=_r();B(E=>Ke(d,`background: ${E??""};`),[()=>nt(a(Ze))]),h(Te,d)}),s(Ce),B(()=>Ke(Ce,`grid-template-columns: repeat(${(a(ie).width||8)??""}, 8px);`)),h(he,Ce)},Ve=he=>{var Ce=br();h(he,Ce)};R(Je,he=>{a(ie).colors&&a(ie).colors.length>0?he(et):he(Ve,!1)})}s(Be),h(pe,Be)}),s(K),h(oe,K)};R(re,oe=>{a(v)&&oe(le)})}B(()=>F(ae,`${a(v)?"▾":"▸"} ${a(v)?"Hide":"Show"} tiles (${t.device.tile_devices.length??""})`)),h(q,se)},ve=q=>{var se=Le(),Y=de(se);{var ae=re=>{var le=wr(),oe=i(le);vt(2),s(le),B(K=>Ke(oe,`background: ${K??""};`),[()=>nt(t.device.color)]),h(re,le)};R(Y,re=>{t.device.has_color&&t.device.color&&re(ae)},!0)}h(q,se)};R(Z,q=>{t.device.has_matrix&&t.device.tile_devices&&t.device.tile_devices.length>0?q(_e):q(ve,!1)},!0)}h(S,N)};R($,S=>{t.device.has_multizone&&t.device.zone_colors&&t.device.zone_colors.length>0?S(ce):S(me,!1)})}s(_),B(()=>{F(u,t.device.serial),F(A,t.device.label),X.disabled=a(n),F(b,a(n)?"...":"Del"),T=Xe(C,1,"badge",null,T,{"badge-power-on":t.device.power_level>0,"badge-power-off":t.device.power_level===0}),F(M,t.device.power_level>0?"ON":"OFF"),F(z,`P${t.device.product??""}`),F(D,`${a(m)?"▾":"▸"} ${a(m)?"Hide":"Show"} metadata`)}),h(e,_),Oe()}qe(["click"]);var kr=k('<div class="no-devices">No devices emulated</div>'),Sr=k('<div class="pagination svelte-qz4xni"><button class="btn btn-sm">Previous</button> <span class="page-info svelte-qz4xni"> </span> <button class="btn btn-sm">Next</button></div>'),Er=k('<div class="devices-grid"></div> <!>',1),Fr=k('<div class="card"><h2 style="display: flex; justify-content: space-between; align-items: center;"><span> </span> <button class="btn btn-delete" title="Remove all devices from server (runtime only)"> </button></h2> <!></div>');function Ir(e,t){Re(t,!0);let n=J(!1),r=J(lt(V.count));async function l(){if(V.count===0){alert("No devices to remove");return}const C=`Remove all ${V.count} device(s) from the server?
3
+
4
+ This will stop all devices from responding to LIFX protocol packets, but will not delete persistent storage.`;if(confirm(C)){f(n,!0);try{const T=await Lt();alert(T.message)}catch(T){alert(T instanceof Error?T.message:"Failed to remove devices")}finally{f(n,!1)}}}let c=De(()=>Math.max(1,Math.ceil(V.count/I.pageSize))),v=De(()=>{const C=(I.currentPage-1)*I.pageSize,T=C+I.pageSize;return V.list.slice(C,T)});function m(){I.currentPage>1&&I.setCurrentPage(I.currentPage-1)}function _(){I.currentPage<a(c)&&I.setCurrentPage(I.currentPage+1)}dt(()=>{if(Math.abs(V.count-a(r))>0){const T=Math.max(1,Math.ceil(V.count/I.pageSize));I.currentPage>T&&I.setCurrentPage(1),f(r,V.count,!0)}});var w=Fr(),y=i(w),g=i(y),u=i(g);s(g);var p=o(g,2);p.__click=l;var A=i(p,!0);s(p),s(y);var X=o(y,2);{var b=C=>{var T=kr();h(C,T)},x=C=>{var T=Er(),M=de(T);ye(M,21,()=>a(v),O=>O.serial,(O,W)=>{xr(O,{get device(){return a(W)}})}),s(M);var L=o(M,2);{var z=O=>{var W=Sr(),j=i(W);j.__click=m;var H=o(j,2),ue=i(H);s(H);var ee=o(H,2);ee.__click=_,s(W),B(()=>{j.disabled=I.currentPage<=1,F(ue,`Page ${I.currentPage??""} of ${a(c)??""}`),ee.disabled=I.currentPage>=a(c)}),h(O,W)};R(L,O=>{a(c)>1&&O(z)})}h(C,T)};R(X,C=>{V.count===0?C(b):C(x,!1)})}s(w),B(()=>{F(u,`Devices (${V.count??""})`),p.disabled=a(n)||V.count===0,F(A,a(n)?"Removing...":"Remove All")}),h(e,w),Oe()}qe(["click"]);var Lr=k('<div class="no-devices svelte-14519bm">No devices emulated</div>'),Cr=k('<div class="zone-mini svelte-14519bm"></div>'),Ar=k('<span class="more-zones svelte-14519bm"> </span>'),Pr=k('<div class="zone-mini-strip svelte-14519bm"><!> <!></div>'),Tr=k('<span class="color-swatch svelte-14519bm"></span>'),zr=k('<span class="no-color svelte-14519bm">-</span>'),Mr=k('<tr class="svelte-14519bm"><td class="cell-serial svelte-14519bm"> </td><td class="cell-label svelte-14519bm"> </td><td class="cell-product svelte-14519bm"> </td><td class="svelte-14519bm"><span> </span></td><td class="svelte-14519bm"><!></td><td class="svelte-14519bm"><button class="btn btn-delete btn-sm"> </button></td></tr>'),Nr=k('<div class="pagination svelte-14519bm"><button class="btn btn-sm">Previous</button> <span class="page-info svelte-14519bm"> </span> <button class="btn btn-sm">Next</button></div>'),Dr=k('<div class="table-container svelte-14519bm"><table class="device-table svelte-14519bm"><thead><tr><th class="svelte-14519bm">Serial</th><th class="svelte-14519bm">Label</th><th class="svelte-14519bm">Product</th><th class="svelte-14519bm">Power</th><th class="svelte-14519bm">Color</th><th class="svelte-14519bm">Actions</th></tr></thead><tbody class="svelte-14519bm"></tbody></table></div> <!>',1),Xr=k('<div class="card"><h2 style="display: flex; justify-content: space-between; align-items: center;"><span> </span> <button class="btn btn-delete" title="Remove all devices from server (runtime only)"> </button></h2> <!></div>');function Rr(e,t){Re(t,!0);let n=J(!1),r=J(null),l=J(lt(V.count));async function c(){if(V.count===0){alert("No devices to remove");return}const L=`Remove all ${V.count} device(s) from the server?
5
+
6
+ This will stop all devices from responding to LIFX protocol packets, but will not delete persistent storage.`;if(confirm(L)){f(n,!0);try{const z=await Lt();alert(z.message)}catch(z){alert(z instanceof Error?z.message:"Failed to remove devices")}finally{f(n,!1)}}}async function v(L){if(confirm(`Delete device ${L}?`)){f(r,L,!0);try{await Jt(L)}catch(z){alert(z instanceof Error?z.message:"Failed to delete device")}finally{f(r,null)}}}function m(L){return{1:"Original 1000",10:"White 800 (Low Voltage)",11:"White 800 (High Voltage)",18:"White 900 BR30",20:"Color 1000 BR30",22:"Color 1000",27:"LIFX A19",28:"LIFX BR30",29:"LIFX+ A19",30:"LIFX+ BR30",31:"LIFX Z",32:"LIFX Z 2",36:"LIFX Downlight",37:"LIFX Downlight",38:"LIFX Beam",43:"LIFX A19",44:"LIFX BR30",45:"LIFX+ A19",46:"LIFX+ BR30",49:"LIFX Mini Color",50:"LIFX Mini Day and Dusk",51:"LIFX Mini White",52:"LIFX GU10",55:"LIFX Tile",57:"LIFX Candle",59:"LIFX Mini Color",60:"LIFX Mini Day and Dusk",61:"LIFX Mini White",62:"LIFX A19",63:"LIFX BR30",64:"LIFX A19 Night Vision",65:"LIFX BR30 Night Vision",68:"LIFX Candle CA",70:"LIFX Switch",71:"LIFX Switch",81:"LIFX Candle Warm to White",82:"LIFX Filament",85:"LIFX A19 HEV",87:"LIFX Candle Color",88:"LIFX BR30 HEV",89:"LIFX A19 Clean",90:"LIFX Color",91:"LIFX Color",94:"LIFX BR30",96:"LIFX Candle White to Warm",97:"LIFX A19",98:"LIFX BR30",99:"LIFX Clean",100:"LIFX Filament Clear",101:"LIFX Filament Amber",109:"LIFX A19 HEV",110:"LIFX BR30 HEV",111:"LIFX Neon",112:"LIFX Lightstrip",120:"LIFX GU10"}[L]||`Product ${L}`}let _=De(()=>Math.max(1,Math.ceil(V.count/I.pageSize))),w=De(()=>{const L=(I.currentPage-1)*I.pageSize,z=L+I.pageSize;return V.list.slice(L,z)});function y(){I.currentPage>1&&I.setCurrentPage(I.currentPage-1)}function g(){I.currentPage<a(_)&&I.setCurrentPage(I.currentPage+1)}dt(()=>{if(Math.abs(V.count-a(l))>0){const z=Math.max(1,Math.ceil(V.count/I.pageSize));I.currentPage>z&&I.setCurrentPage(1),f(l,V.count,!0)}});var u=Xr(),p=i(u),A=i(p),X=i(A);s(A);var b=o(A,2);b.__click=c;var x=i(b,!0);s(b),s(p);var C=o(p,2);{var T=L=>{var z=Lr();h(L,z)},M=L=>{var z=Dr(),O=de(z),W=i(O),j=o(i(W));ye(j,21,()=>a(w),ee=>ee.serial,(ee,P)=>{var Q=Mr(),fe=i(Q),xe=i(fe,!0);s(fe);var be=o(fe),ne=i(be,!0);s(be);var D=o(be),G=i(D,!0);s(D);var te=o(D),$=i(te);let ce;var me=i($,!0);s($),s(te);var S=o(te),N=i(S);{var Z=Y=>{var ae=Pr(),re=i(ae);ye(re,17,()=>a(P).zone_colors.slice(0,16),Ie,(K,pe)=>{var ie=Cr();B(Pe=>Ke(ie,`background: ${Pe??""};`),[()=>nt(a(pe))]),h(K,ie)});var le=o(re,2);{var oe=K=>{var pe=Ar(),ie=i(pe);s(pe),B(()=>F(ie,`+${a(P).zone_colors.length-16}`)),h(K,pe)};R(le,K=>{a(P).zone_colors.length>16&&K(oe)})}s(ae),h(Y,ae)},_e=Y=>{var ae=Le(),re=de(ae);{var le=K=>{var pe=Tr();B(ie=>Ke(pe,`background: ${ie??""};`),[()=>nt(a(P).color)]),h(K,pe)},oe=K=>{var pe=zr();h(K,pe)};R(re,K=>{a(P).has_color&&a(P).color?K(le):K(oe,!1)},!0)}h(Y,ae)};R(N,Y=>{a(P).has_multizone&&a(P).zone_colors&&a(P).zone_colors.length>0?Y(Z):Y(_e,!1)})}s(S);var ve=o(S),q=i(ve);q.__click=()=>v(a(P).serial);var se=i(q,!0);s(q),s(ve),s(Q),B(Y=>{F(xe,a(P).serial),F(ne,a(P).label),F(G,Y),ce=Xe($,1,"power-badge svelte-14519bm",null,ce,{"power-on":a(P).power_level>0,"power-off":a(P).power_level===0}),F(me,a(P).power_level>0?"ON":"OFF"),q.disabled=a(r)===a(P).serial,F(se,a(r)===a(P).serial?"...":"Del")},[()=>m(a(P).product)]),h(ee,Q)}),s(j),s(W),s(O);var H=o(O,2);{var ue=ee=>{var P=Nr(),Q=i(P);Q.__click=y;var fe=o(Q,2),xe=i(fe);s(fe);var be=o(fe,2);be.__click=g,s(P),B(()=>{Q.disabled=I.currentPage<=1,F(xe,`Page ${I.currentPage??""} of ${a(_)??""}`),be.disabled=I.currentPage>=a(_)}),h(ee,P)};R(H,ee=>{a(_)>1&&ee(ue)})}h(L,z)};R(C,L=>{V.count===0?L(T):L(M,!1)})}s(u),B(()=>{F(X,`Devices (${V.count??""})`),b.disabled=a(n)||V.count===0,F(x,a(n)?"Removing...":"Remove All")}),h(e,u),Oe()}qe(["click"]);var Or=k('<div class="activity-disabled svelte-i8zsux"><p class="svelte-i8zsux">Activity logging is disabled.</p> <p class="hint svelte-i8zsux">Start the emulator with <code class="svelte-i8zsux">--api-activity</code> to enable packet logging.</p></div>'),Br=k('<button class="btn btn-sm">Clear</button>'),Hr=k('<div style="color: var(--text-dimmed);">No activity yet</div>'),Vr=k('<div style="color: var(--text-dimmed);">No matching activity</div>'),Ur=k('<div class="activity-item"><span class="activity-time"> </span> <span> </span> <span class="activity-packet"> </span> <span class="device-serial"> </span> <span style="color: var(--text-dimmed);"> </span></div>'),Wr=k('<div class="activity-filters"><div class="filter-group"><label for="filter-direction">Direction:</label> <select id="filter-direction"><option>All</option><option>RX</option><option>TX</option></select></div> <div class="filter-group"><label for="filter-device">Device/Target:</label> <input id="filter-device" type="text" placeholder="Filter by device..."/></div> <div class="filter-group"><label for="filter-packet">Packet:</label> <input id="filter-packet" type="text" placeholder="Filter by packet..."/></div> <!></div> <div class="activity-log"><!></div>',1),jr=k('<div class="card"><h2>Recent Activity</h2> <!></div>');function qr(e,t){Re(t,!0);let n=J("all"),r=J(""),l=J("");function c(g){return new Date(g*1e3).toLocaleTimeString()}let v=De(()=>{let g=ot.reversed;if(a(n)!=="all"&&(g=g.filter(u=>u.direction===a(n))),a(r)){const u=a(r).toLowerCase();g=g.filter(p=>p.device?.toLowerCase().includes(u)||p.target?.toLowerCase().includes(u))}if(a(l)){const u=a(l).toLowerCase();g=g.filter(p=>p.packet_name.toLowerCase().includes(u))}return g});var m=jr(),_=o(i(m),2);{var w=g=>{var u=Or();h(g,u)},y=g=>{var u=Wr(),p=de(u),A=i(p),X=o(i(A),2),b=i(X);b.value=b.__value="all";var x=o(b);x.value=x.__value="rx";var C=o(x);C.value=C.__value="tx",s(X),s(A);var T=o(A,2),M=o(i(T),2);Se(M),s(T);var L=o(T,2),z=o(i(L),2);Se(z),s(L);var O=o(L,2);{var W=P=>{var Q=Br();Q.__click=()=>{f(n,"all"),f(r,""),f(l,"")},h(P,Q)};R(O,P=>{(a(n)!=="all"||a(r)||a(l))&&P(W)})}s(p);var j=o(p,2),H=i(j);{var ue=P=>{var Q=Hr();h(P,Q)},ee=P=>{var Q=Le(),fe=de(Q);{var xe=ne=>{var D=Vr();h(ne,D)},be=ne=>{var D=Le(),G=de(D);ye(G,17,()=>a(v),Ie,(te,$)=>{var ce=Ur(),me=i(ce),S=i(me,!0);s(me);var N=o(me,2),Z=i(N,!0);s(N);var _e=o(N,2),ve=i(_e,!0);s(_e);var q=o(_e,2),se=i(q,!0);s(q);var Y=o(q,2),ae=i(Y,!0);s(Y),s(ce),B((re,le)=>{F(S,re),Xe(N,1,Pa(a($).direction==="rx"?"activity-rx":"activity-tx")),F(Z,le),F(ve,a($).packet_name),F(se,a($).device||a($).target||"N/A"),F(ae,a($).addr)},[()=>c(a($).timestamp),()=>a($).direction.toUpperCase()]),h(te,ce)}),h(ne,D)};R(fe,ne=>{a(v).length===0?ne(xe):ne(be,!1)},!0)}h(P,Q)};R(H,P=>{ot.count===0?P(ue):P(ee,!1)})}s(j),It(X,()=>a(n),P=>f(n,P)),Fe(M,()=>a(r),P=>f(r,P)),Fe(z,()=>a(l),P=>f(l,P)),h(g,u)};R(_,g=>{je.value.activity_enabled?g(y,!1):g(w)})}s(m),h(e,m),Oe()}qe(["click"]);var Gr=k("<button> </button>"),Jr=k("<option> </option>"),Yr=k("<option> </option>"),Zr=k('<div class="form-group" style="margin-top: 12px;"><label for="identifier"> </label> <select id="identifier"><!></select></div>'),Kr=k('<div class="inline-row svelte-15drtwo"><input type="text" placeholder="Packet type (e.g., 101)" style="width: 140px;" class="svelte-15drtwo"/> <input type="number" min="0" max="1" step="0.1" placeholder="Drop rate" style="width: 100px;" class="svelte-15drtwo"/> <button type="button" class="btn btn-delete btn-sm">X</button></div>'),Qr=k('<div class="inline-row svelte-15drtwo"><input type="text" placeholder="Packet type (e.g., 101)" style="width: 140px;" class="svelte-15drtwo"/> <input type="number" min="0" step="0.1" placeholder="Delay (s)" style="width: 100px;" class="svelte-15drtwo"/> <button type="button" class="btn btn-delete btn-sm">X</button></div>'),$r=k('<button type="button" class="btn btn-sm">Clear</button>'),es=k('<p class="message error svelte-15drtwo"> </p>'),ts=k('<p class="message success svelte-15drtwo"> </p>'),as=k('<div class="card"><h2>Scenario Configuration</h2> <div class="scope-tabs svelte-15drtwo"></div> <!> <div class="scenario-form svelte-15drtwo"><div class="form-section svelte-15drtwo"><div class="section-header svelte-15drtwo"><span class="section-title svelte-15drtwo">Drop Packets</span> <button type="button" class="btn btn-sm">+ Add</button></div> <p class="section-desc svelte-15drtwo">Specify packet types to drop (0.0 = never, 1.0 = always)</p> <!></div> <div class="form-section svelte-15drtwo"><div class="section-header svelte-15drtwo"><span class="section-title svelte-15drtwo">Response Delays</span> <button type="button" class="btn btn-sm">+ Add</button></div> <p class="section-desc svelte-15drtwo">Add delay (seconds) before responding to packet types</p> <!></div> <div class="form-section svelte-15drtwo"><label for="malformed" class="section-title svelte-15drtwo">Malformed Packets</label> <p class="section-desc svelte-15drtwo">Packet types to send with corrupted payloads (comma-separated)</p> <input id="malformed" type="text" placeholder="e.g., 101, 102, 118" class="svelte-15drtwo"/></div> <div class="form-section svelte-15drtwo"><label for="invalid" class="section-title svelte-15drtwo">Invalid Field Values</label> <p class="section-desc svelte-15drtwo">Packet types to send with 0xFF bytes (comma-separated)</p> <input id="invalid" type="text" placeholder="e.g., 101, 102" class="svelte-15drtwo"/></div> <div class="form-section svelte-15drtwo"><div class="section-header svelte-15drtwo"><span class="section-title svelte-15drtwo">Firmware Version Override</span> <!></div> <p class="section-desc svelte-15drtwo">Override reported firmware version (major.minor)</p> <div class="inline-row svelte-15drtwo"><input type="number" min="0" max="255" placeholder="Major" style="width: 80px;" class="svelte-15drtwo"/> <span>.</span> <input type="number" min="0" max="255" placeholder="Minor" style="width: 80px;" class="svelte-15drtwo"/></div></div> <div class="form-section svelte-15drtwo"><label for="partial" class="section-title svelte-15drtwo">Partial Responses</label> <p class="section-desc svelte-15drtwo">Packet types to send with incomplete data (comma-separated)</p> <input id="partial" type="text" placeholder="e.g., 506, 512" class="svelte-15drtwo"/></div> <div class="form-section svelte-15drtwo"><label class="checkbox-label svelte-15drtwo"><input type="checkbox" class="svelte-15drtwo"/> <span>Send Unhandled</span></label> <p class="section-desc svelte-15drtwo">Return StateUnhandled for unknown packet types</p></div></div> <!> <!> <div class="actions svelte-15drtwo"><button class="btn"> </button> <button class="btn btn-delete"> </button></div></div>');function rs(e,t){Re(t,!0);const n=["matrix","extended_multizone","multizone","hev","infrared","color","basic"];let r=J("global"),l=J(""),c=J(!1),v=J(null),m=J(null),_=J(lt([])),w=J(lt([])),y=J(""),g=J(""),u=J(""),p=J(""),A=J(""),X=J(!1);function b(d){return{global:"devices",device:"devices",type:"types",location:"locations",group:"groups"}[d]}let x=De(()=>()=>{switch(a(r)){case"device":return V.list.map(d=>d.serial);case"type":return n;case"location":return[...new Set(V.list.map(d=>d.location_label).filter(Boolean))];case"group":return[...new Set(V.list.map(d=>d.group_label).filter(Boolean))];default:return[]}}),C=De(()=>()=>{switch(a(r)){case"global":return Ye.global;case"device":return a(l)?Ye.devices[a(l)]:null;case"type":return a(l)?Ye.types[a(l)]:null;case"location":return a(l)?Ye.locations[a(l)]:null;case"group":return a(l)?Ye.groups[a(l)]:null;default:return null}});function T(d){if(!d){f(_,[],!0),f(w,[],!0),f(y,""),f(g,""),f(u,""),f(p,""),f(A,""),f(X,!0);return}d.drop_packets&&Object.keys(d.drop_packets).length>0?f(_,Object.entries(d.drop_packets).map(([E,U])=>({packetType:E,rate:String(U)})),!0):f(_,[],!0),d.response_delays&&Object.keys(d.response_delays).length>0?f(w,Object.entries(d.response_delays).map(([E,U])=>({packetType:E,delay:String(U)})),!0):f(w,[],!0),f(y,d.malformed_packets?.join(", ")||"",!0),f(g,d.invalid_field_values?.join(", ")||"",!0),f(A,d.partial_responses?.join(", ")||"",!0),d.firmware_version?(f(u,String(d.firmware_version[0]),!0),f(p,String(d.firmware_version[1]),!0)):(f(u,""),f(p,"")),f(X,d.send_unhandled??!0,!0)}function M(){const d={};if(a(_).length>0){d.drop_packets={};for(const{packetType:E,rate:U}of a(_))E&&U&&(d.drop_packets[E]=parseFloat(U))}if(a(w).length>0){d.response_delays={};for(const{packetType:E,delay:U}of a(w))E&&U&&(d.response_delays[E]=parseFloat(U))}return a(y).trim()&&(d.malformed_packets=a(y).split(",").map(E=>parseInt(E.trim(),10)).filter(E=>!isNaN(E))),a(g).trim()&&(d.invalid_field_values=a(g).split(",").map(E=>parseInt(E.trim(),10)).filter(E=>!isNaN(E))),a(A).trim()&&(d.partial_responses=a(A).split(",").map(E=>parseInt(E.trim(),10)).filter(E=>!isNaN(E))),a(u)&&a(p)&&(d.firmware_version=[parseInt(a(u),10),parseInt(a(p),10)]),d.send_unhandled=a(X),d}async function L(){f(v,null),f(m,null),f(c,!0);try{const d=M();if(a(r)==="global")await Ja(d);else{if(!a(l))throw new Error("Please select an identifier");await Za(b(a(r)),a(l),d)}f(m,"Scenario applied successfully"),setTimeout(()=>f(m,null),3e3)}catch(d){f(v,d instanceof Error?d.message:"Failed to apply scenario",!0)}finally{f(c,!1)}}async function z(){f(v,null),f(m,null),f(c,!0);try{if(a(r)==="global")await Ya();else{if(!a(l))throw new Error("Please select an identifier");await Ka(b(a(r)),a(l))}T(null),f(m,"Scenario cleared successfully"),setTimeout(()=>f(m,null),3e3)}catch(d){f(v,d instanceof Error?d.message:"Failed to clear scenario",!0)}finally{f(c,!1)}}function O(){f(_,[...a(_),{packetType:"",rate:"1.0"}],!0)}function W(d){f(_,a(_).filter((E,U)=>U!==d),!0)}function j(){f(w,[...a(w),{packetType:"",delay:"0.5"}],!0)}function H(d){f(w,a(w).filter((E,U)=>U!==d),!0)}function ue(){f(u,""),f(p,"")}dt(()=>{a(r)==="global"?T(a(C)()):a(l)?T(a(C)()):T(null)}),dt(()=>{const d=a(x)();a(r)!=="global"&&d.length>0&&!d.includes(a(l))&&f(l,d[0],!0)});var ee=as(),P=o(i(ee),2);ye(P,20,()=>["global","device","type","location","group"],Ie,(d,E)=>{var U=Gr();let ge;U.__click=()=>f(r,E,!0);var ke=i(U,!0);s(U),B(Ee=>{ge=Xe(U,1,"scope-tab svelte-15drtwo",null,ge,{active:a(r)===E}),F(ke,Ee)},[()=>E.charAt(0).toUpperCase()+E.slice(1)]),h(d,U)}),s(P);var Q=o(P,2);{var fe=d=>{var E=Zr(),U=i(E),ge=i(U);s(U);var ke=o(U,2),Ee=i(ke);{var tt=Ae=>{var Ue=Jr(),ut=i(Ue);s(Ue),Ue.value=Ue.__value="",B(()=>F(ut,`No ${a(r)??""}s available`)),h(Ae,Ue)},ze=Ae=>{var Ue=Le(),ut=de(Ue);ye(ut,17,()=>a(x)(),Ie,(Yt,ft)=>{var at=Yr(),Zt=i(at,!0);s(at);var Ct={};B(()=>{F(Zt,a(ft)),Ct!==(Ct=a(ft))&&(at.value=(at.__value=a(ft))??"")}),h(Yt,at)}),h(Ae,Ue)};R(Ee,Ae=>{a(x)().length===0?Ae(tt):Ae(ze,!1)})}s(ke),s(E),B(Ae=>F(ge,`${Ae??""}:`),[()=>a(r).charAt(0).toUpperCase()+a(r).slice(1)]),It(ke,()=>a(l),Ae=>f(l,Ae)),h(d,E)};R(Q,d=>{a(r)!=="global"&&d(fe)})}var xe=o(Q,2),be=i(xe),ne=i(be),D=o(i(ne),2);D.__click=O,s(ne);var G=o(ne,4);ye(G,17,()=>a(_),Ie,(d,E,U)=>{var ge=Kr(),ke=i(ge);Se(ke);var Ee=o(ke,2);Se(Ee);var tt=o(Ee,2);tt.__click=()=>W(U),s(ge),Fe(ke,()=>a(E).packetType,ze=>a(E).packetType=ze),Fe(Ee,()=>a(E).rate,ze=>a(E).rate=ze),h(d,ge)}),s(be);var te=o(be,2),$=i(te),ce=o(i($),2);ce.__click=j,s($);var me=o($,4);ye(me,17,()=>a(w),Ie,(d,E,U)=>{var ge=Qr(),ke=i(ge);Se(ke);var Ee=o(ke,2);Se(Ee);var tt=o(Ee,2);tt.__click=()=>H(U),s(ge),Fe(ke,()=>a(E).packetType,ze=>a(E).packetType=ze),Fe(Ee,()=>a(E).delay,ze=>a(E).delay=ze),h(d,ge)}),s(te);var S=o(te,2),N=o(i(S),4);Se(N),s(S);var Z=o(S,2),_e=o(i(Z),4);Se(_e),s(Z);var ve=o(Z,2),q=i(ve),se=o(i(q),2);{var Y=d=>{var E=$r();E.__click=ue,h(d,E)};R(se,d=>{(a(u)||a(p))&&d(Y)})}s(q);var ae=o(q,4),re=i(ae);Se(re);var le=o(re,4);Se(le),s(ae),s(ve);var oe=o(ve,2),K=o(i(oe),4);Se(K),s(oe);var pe=o(oe,2),ie=i(pe),Pe=i(ie);Se(Pe),vt(2),s(ie),vt(2),s(pe),s(xe);var Be=o(xe,2);{var He=d=>{var E=es(),U=i(E,!0);s(E),B(()=>F(U,a(v))),h(d,E)};R(Be,d=>{a(v)&&d(He)})}var Je=o(Be,2);{var et=d=>{var E=ts(),U=i(E,!0);s(E),B(()=>F(U,a(m))),h(d,E)};R(Je,d=>{a(m)&&d(et)})}var Ve=o(Je,2),he=i(Ve);he.__click=L;var Ce=i(he,!0);s(he);var Te=o(he,2);Te.__click=z;var Ze=i(Te,!0);s(Te),s(Ve),s(ee),B(()=>{he.disabled=a(c)||a(r)!=="global"&&!a(l),F(Ce,a(c)?"Applying...":"Apply"),Te.disabled=a(c)||a(r)!=="global"&&!a(l),F(Ze,a(c)?"Clearing...":"Clear")}),Fe(N,()=>a(y),d=>f(y,d)),Fe(_e,()=>a(g),d=>f(g,d)),Fe(re,()=>a(u),d=>f(u,d)),Fe(le,()=>a(p),d=>f(p,d)),Fe(K,()=>a(A),d=>f(A,d)),Ra(Pe,()=>a(X),d=>f(X,d)),h(e,ee),Oe()}qe(["click"]);var ss=k('<span class="tab-count svelte-1uha8ag"> </span>'),is=k('<span class="tab-count svelte-1uha8ag"> </span>'),ns=k("<button> <!></button>"),ls=k("<!> <!>",1),os=k('<div class="container"><!> <!> <div class="tabs svelte-1uha8ag"></div> <div class="tab-content svelte-1uha8ag"><!></div></div>');function bs(e,t){Re(t,!1);const n=[{id:"devices",label:"Devices"},{id:"activity",label:"Activity"},{id:"scenarios",label:"Scenarios"}];Xt(()=>{Qe.connect()}),Kt(()=>{Qe.disconnect()}),kt();var r=os(),l=i(r);Va(l,{});var c=o(l,2);{var v=u=>{ja(u,{})};R(c,u=>{I.showStats&&u(v)})}var m=o(c,2);ye(m,5,()=>n,Ie,(u,p)=>{var A=ns();let X;A.__click=()=>I.setActiveTab(a(p).id);var b=i(A),x=o(b);{var C=M=>{var L=ss(),z=i(L,!0);s(L),B(()=>F(z,V.count)),h(M,L)},T=M=>{var L=Le(),z=de(L);{var O=W=>{var j=is(),H=i(j,!0);s(j),B(()=>F(H,ot.count)),h(W,j)};R(z,W=>{a(p).id==="activity"&&W(O)},!0)}h(M,L)};R(x,M=>{a(p).id==="devices"?M(C):M(T,!1)})}s(A),B(()=>{X=Xe(A,1,"tab svelte-1uha8ag",null,X,{active:I.activeTab===a(p).id}),F(b,`${a(p).label??""} `)}),h(u,A)}),s(m);var _=o(m,2),w=i(_);{var y=u=>{var p=ls(),A=de(p);rr(A,{});var X=o(A,2);{var b=C=>{Rr(C,{})},x=C=>{Ir(C,{})};R(X,C=>{I.viewMode==="table"?C(b):C(x,!1)})}h(u,p)},g=u=>{var p=Le(),A=de(p);{var X=x=>{qr(x,{})},b=x=>{var C=Le(),T=de(C);{var M=L=>{rs(L,{})};R(T,L=>{I.activeTab==="scenarios"&&L(M)},!0)}h(x,C)};R(A,x=>{I.activeTab==="activity"?x(X):x(b,!1)},!0)}h(u,p)};R(w,u=>{I.activeTab==="devices"?u(y):u(g,!1)})}s(_),s(r),h(e,r),Oe()}qe(["click"]);export{bs as component};
@@ -0,0 +1 @@
1
+ {"version":"1770117930907"}
@@ -0,0 +1,38 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>LIFX Emulator Monitor</title>
7
+
8
+ <link rel="modulepreload" href="/_app/immutable/entry/start.Nqz6UJJT.js">
9
+ <link rel="modulepreload" href="/_app/immutable/chunks/MAGDeS2Z.js">
10
+ <link rel="modulepreload" href="/_app/immutable/chunks/DfIkQq0Y.js">
11
+ <link rel="modulepreload" href="/_app/immutable/chunks/yhjkpkcN.js">
12
+ <link rel="modulepreload" href="/_app/immutable/entry/app.Dhwm664s.js">
13
+ <link rel="modulepreload" href="/_app/immutable/chunks/BaoxLdOF.js">
14
+ <link rel="modulepreload" href="/_app/immutable/chunks/Binc8JbE.js">
15
+ <link rel="modulepreload" href="/_app/immutable/chunks/BTLkiQR5.js">
16
+ <link rel="modulepreload" href="/_app/immutable/chunks/N3z8axFy.js">
17
+ </head>
18
+ <body data-sveltekit-preload-data="hover">
19
+ <div style="display: contents">
20
+ <script>
21
+ {
22
+ __sveltekit_p60p87 = {
23
+ base: ""
24
+ };
25
+
26
+ const element = document.currentScript.parentElement;
27
+
28
+ Promise.all([
29
+ import("/_app/immutable/entry/start.Nqz6UJJT.js"),
30
+ import("/_app/immutable/entry/app.Dhwm664s.js")
31
+ ]).then(([kit, app]) => {
32
+ kit.start(app, element);
33
+ });
34
+ }
35
+ </script>
36
+ </div>
37
+ </body>
38
+ </html>
@@ -0,0 +1,3 @@
1
+ # allow crawling everything by default
2
+ User-agent: *
3
+ Disallow:
@@ -0,0 +1,316 @@
1
+ """Configuration file support for lifx-emulator CLI."""
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import yaml
10
+ from pydantic import BaseModel, Field, field_validator, model_validator
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ AUTO_DETECT_FILENAMES = ("lifx-emulator.yaml", "lifx-emulator.yml")
15
+ ENV_VAR = "LIFX_EMULATOR_CONFIG"
16
+
17
+ _SERIAL_PATTERN = re.compile(r"^[0-9a-fA-F]{12}$")
18
+
19
+
20
+ class HsbkConfig(BaseModel):
21
+ """HSBK color value supporting both dict and [h, s, b, k] list input."""
22
+
23
+ hue: int = 0
24
+ saturation: int = 0
25
+ brightness: int = 65535
26
+ kelvin: int = 3500
27
+
28
+ @model_validator(mode="before")
29
+ @classmethod
30
+ def accept_list_form(cls, data):
31
+ """Convert [h, s, b, k] list to dict form."""
32
+ if isinstance(data, list | tuple):
33
+ if len(data) != 4:
34
+ msg = (
35
+ "HSBK list must have exactly 4 elements"
36
+ " [hue, saturation, brightness, kelvin]"
37
+ )
38
+ raise ValueError(msg)
39
+ return {
40
+ "hue": data[0],
41
+ "saturation": data[1],
42
+ "brightness": data[2],
43
+ "kelvin": data[3],
44
+ }
45
+ return data
46
+
47
+ @field_validator("hue", "saturation", "brightness")
48
+ @classmethod
49
+ def validate_uint16(cls, v: int) -> int:
50
+ if not 0 <= v <= 65535:
51
+ msg = "Value must be between 0 and 65535"
52
+ raise ValueError(msg)
53
+ return v
54
+
55
+ @field_validator("kelvin")
56
+ @classmethod
57
+ def validate_kelvin(cls, v: int) -> int:
58
+ if not 1500 <= v <= 9000:
59
+ msg = "Kelvin must be between 1500 and 9000"
60
+ raise ValueError(msg)
61
+ return v
62
+
63
+
64
+ class ScenarioDefinition(BaseModel):
65
+ """Scenario configuration for a single scope level."""
66
+
67
+ drop_packets: dict[int, float] | None = None
68
+ response_delays: dict[int, float] | None = None
69
+ malformed_packets: list[int] | None = None
70
+ invalid_field_values: list[int] | None = None
71
+ firmware_version: tuple[int, int] | None = None
72
+ partial_responses: list[int] | None = None
73
+ send_unhandled: bool | None = None
74
+
75
+ @field_validator("drop_packets", mode="before")
76
+ @classmethod
77
+ def convert_drop_packets_keys(cls, v):
78
+ """Convert string keys to integers (YAML keys are often strings)."""
79
+ if isinstance(v, dict):
80
+ return {int(k): float(val) for k, val in v.items()}
81
+ return v
82
+
83
+ @field_validator("response_delays", mode="before")
84
+ @classmethod
85
+ def convert_response_delays_keys(cls, v):
86
+ """Convert string keys to integers (YAML keys are often strings)."""
87
+ if isinstance(v, dict):
88
+ return {int(k): float(val) for k, val in v.items()}
89
+ return v
90
+
91
+
92
+ class ScenariosConfig(BaseModel):
93
+ """Scenarios configuration across all scope levels."""
94
+
95
+ global_scenario: ScenarioDefinition | None = Field(None, alias="global")
96
+ devices: dict[str, ScenarioDefinition] | None = None
97
+ types: dict[str, ScenarioDefinition] | None = None
98
+ locations: dict[str, ScenarioDefinition] | None = None
99
+ groups: dict[str, ScenarioDefinition] | None = None
100
+
101
+ model_config = {"populate_by_name": True}
102
+
103
+
104
+ class DeviceDefinition(BaseModel):
105
+ """A single device definition in the config file."""
106
+
107
+ product_id: int
108
+ serial: str | None = None
109
+ label: str | None = None
110
+ power_level: int | None = None
111
+ color: HsbkConfig | None = None
112
+ location: str | None = None
113
+ group: str | None = None
114
+ zone_count: int | None = None
115
+ zone_colors: list[HsbkConfig] | None = None
116
+ infrared_brightness: int | None = None
117
+ hev_cycle_duration: int | None = None
118
+ hev_indication: bool | None = None
119
+ tile_count: int | None = None
120
+ tile_width: int | None = None
121
+ tile_height: int | None = None
122
+
123
+ @field_validator("serial")
124
+ @classmethod
125
+ def validate_serial(cls, v: str | None) -> str | None:
126
+ if v is not None and not _SERIAL_PATTERN.match(v):
127
+ msg = "serial must be exactly 12 hex characters"
128
+ raise ValueError(msg)
129
+ return v
130
+
131
+ @field_validator("power_level")
132
+ @classmethod
133
+ def validate_power_level(cls, v: int | None) -> int | None:
134
+ if v is not None and v not in (0, 65535):
135
+ msg = "power_level must be 0 (off) or 65535 (on)"
136
+ raise ValueError(msg)
137
+ return v
138
+
139
+ @field_validator("infrared_brightness")
140
+ @classmethod
141
+ def validate_infrared_brightness(cls, v: int | None) -> int | None:
142
+ if v is not None and not 0 <= v <= 65535:
143
+ msg = "infrared_brightness must be between 0 and 65535"
144
+ raise ValueError(msg)
145
+ return v
146
+
147
+ @field_validator("hev_cycle_duration")
148
+ @classmethod
149
+ def validate_hev_cycle_duration(cls, v: int | None) -> int | None:
150
+ if v is not None and v < 0:
151
+ msg = "hev_cycle_duration must be non-negative"
152
+ raise ValueError(msg)
153
+ return v
154
+
155
+
156
+ class EmulatorConfig(BaseModel):
157
+ """Configuration file schema for lifx-emulator."""
158
+
159
+ # Server options
160
+ bind: str | None = None
161
+ port: int | None = None
162
+ verbose: bool | None = None
163
+
164
+ # Storage & Persistence
165
+ persistent: bool | None = None
166
+ persistent_scenarios: bool | None = None
167
+
168
+ # HTTP API Server
169
+ api: bool | None = None
170
+ api_host: str | None = None
171
+ api_port: int | None = None
172
+ api_activity: bool | None = None
173
+ browser: bool | None = None
174
+
175
+ # Device creation (counts)
176
+ products: list[int] | None = None
177
+ color: int | None = None
178
+ color_temperature: int | None = None
179
+ infrared: int | None = None
180
+ hev: int | None = None
181
+ multizone: int | None = None
182
+ tile: int | None = None
183
+ switch: int | None = None
184
+
185
+ # Multizone options
186
+ multizone_zones: int | None = None
187
+ multizone_extended: bool | None = None
188
+
189
+ # Tile/Matrix options
190
+ tile_count: int | None = None
191
+ tile_width: int | None = None
192
+ tile_height: int | None = None
193
+
194
+ # Serial number options
195
+ serial_prefix: str | None = None
196
+ serial_start: int | None = None
197
+
198
+ # Per-device definitions
199
+ devices: list[DeviceDefinition] | None = None
200
+
201
+ # Scenario configuration
202
+ scenarios: ScenariosConfig | None = None
203
+
204
+ @field_validator("serial_prefix")
205
+ @classmethod
206
+ def validate_serial_prefix(cls, v: str | None) -> str | None:
207
+ if v is not None and (
208
+ len(v) != 6 or not all(c in "0123456789abcdefABCDEF" for c in v)
209
+ ):
210
+ msg = "serial_prefix must be exactly 6 hex characters"
211
+ raise ValueError(msg)
212
+ return v
213
+
214
+ model_config = {"extra": "forbid"}
215
+
216
+
217
+ def resolve_config_path(config_flag: str | None) -> Path | None:
218
+ """Resolve the config file path from flag, env var, or auto-detect.
219
+
220
+ Priority: --config flag > LIFX_EMULATOR_CONFIG env var > auto-detect in cwd.
221
+ Returns None if no config file is found.
222
+ """
223
+ # 1. Explicit --config flag
224
+ if config_flag is not None:
225
+ path = Path(config_flag)
226
+ if not path.is_file():
227
+ msg = f"Config file not found: {path}"
228
+ raise FileNotFoundError(msg)
229
+ return path
230
+
231
+ # 2. Environment variable
232
+ env_path = os.environ.get(ENV_VAR)
233
+ if env_path:
234
+ path = Path(env_path)
235
+ if not path.is_file():
236
+ msg = f"Config file from {ENV_VAR} not found: {path}"
237
+ raise FileNotFoundError(msg)
238
+ return path
239
+
240
+ # 3. Auto-detect in current working directory
241
+ for filename in AUTO_DETECT_FILENAMES:
242
+ path = Path.cwd() / filename
243
+ if path.is_file():
244
+ return path
245
+
246
+ return None
247
+
248
+
249
+ def load_config(path: Path) -> EmulatorConfig:
250
+ """Load and validate a config file from the given path."""
251
+ with open(path) as f:
252
+ raw = yaml.safe_load(f)
253
+
254
+ if raw is None:
255
+ return EmulatorConfig()
256
+
257
+ if not isinstance(raw, dict):
258
+ msg = f"Config file must contain a YAML mapping, got {type(raw).__name__}"
259
+ raise ValueError(msg)
260
+
261
+ return EmulatorConfig.model_validate(raw)
262
+
263
+
264
+ def merge_config(
265
+ config: EmulatorConfig,
266
+ cli_overrides: dict[str, Any],
267
+ ) -> dict[str, Any]:
268
+ """Merge config file values with CLI overrides.
269
+
270
+ CLI overrides (non-None values) take priority over config file values.
271
+ Returns a flat dict of final parameter values with defaults applied.
272
+ """
273
+ defaults: dict[str, Any] = {
274
+ "bind": "127.0.0.1",
275
+ "port": 56700,
276
+ "verbose": False,
277
+ "persistent": False,
278
+ "persistent_scenarios": False,
279
+ "api": False,
280
+ "api_host": "127.0.0.1",
281
+ "api_port": 8080,
282
+ "api_activity": True,
283
+ "browser": False,
284
+ "products": None,
285
+ "color": 0,
286
+ "color_temperature": 0,
287
+ "infrared": 0,
288
+ "hev": 0,
289
+ "multizone": 0,
290
+ "tile": 0,
291
+ "switch": 0,
292
+ "multizone_zones": None,
293
+ "multizone_extended": True,
294
+ "tile_count": None,
295
+ "tile_width": None,
296
+ "tile_height": None,
297
+ "serial_prefix": "d073d5",
298
+ "serial_start": 1,
299
+ "devices": None,
300
+ }
301
+
302
+ # Start with defaults
303
+ result = dict(defaults)
304
+
305
+ # Layer config file values (override defaults where set)
306
+ config_dict = config.model_dump(exclude_none=True)
307
+ for key, value in config_dict.items():
308
+ if key in result:
309
+ result[key] = value
310
+
311
+ # Layer CLI overrides (override config where explicitly set)
312
+ for key, value in cli_overrides.items():
313
+ if value is not None and key in result:
314
+ result[key] = value
315
+
316
+ return result
@@ -1,19 +0,0 @@
1
- lifx_emulator_app/__init__.py,sha256=AJahGiWKb8U8yLQbJX21takbf-SoxDxMOGxjJeM7M5c,222
2
- lifx_emulator_app/__main__.py,sha256=f_VwgZfwOdp3zQshv8JQqBX7gocE9ATZbZ3wQndlBvo,22060
3
- lifx_emulator_app/api/__init__.py,sha256=bpdhugx7PAop5IQhkKnWE-a1hfI9gJoJiNRG2CCWe5A,651
4
- lifx_emulator_app/api/app.py,sha256=QyHSzb0BYk341PlWLIcgHJHOYnFetcG0FQsVlVY_OX8,5126
5
- lifx_emulator_app/api/models.py,sha256=fiX9hDmR1C12tzet-kGPmbaG_qiZfiprgy-uxwIcEsE,3970
6
- lifx_emulator_app/api/mappers/__init__.py,sha256=-lGAg-s16eTMl2_D-3bPu-EMqD2kaPzXHqSrKCnmW2w,156
7
- lifx_emulator_app/api/mappers/device_mapper.py,sha256=WAo-_PJ2kX3J4GUW_Sjoype1d_uaIh-PtXPfMUAujUY,4155
8
- lifx_emulator_app/api/routers/__init__.py,sha256=TKPypociZ_uo2YCyW5zIeUfalUh_PkzCVt31MHg9ZPc,381
9
- lifx_emulator_app/api/routers/devices.py,sha256=5_0Id09Z6yOB3s-uAEAU4oNribV49IitC0-_Bmx3bCk,4218
10
- lifx_emulator_app/api/routers/monitoring.py,sha256=i82_s61caYd9UvMb4MqWPLP7LuFh5KN8Qkw4_dZr3O0,1460
11
- lifx_emulator_app/api/routers/scenarios.py,sha256=pWXTliY9MIk-DCxDOZ1cjeAFgHn79ExvY-6lj-yHPWk,9747
12
- lifx_emulator_app/api/services/__init__.py,sha256=cdKZItYE-KkMX44V9xJW_PHHnJoAw5I7Uw1a1YKHgMI,285
13
- lifx_emulator_app/api/services/device_service.py,sha256=A2rCuZ1aAJ1tThKM6BYorKAjlaDgDaFCT5C9KUHfAvc,6303
14
- lifx_emulator_app/api/static/dashboard.js,sha256=eJOtBzTLRPYmMVuft5GC8r7Ae6x_JWZs9nqrmKaOILA,20177
15
- lifx_emulator_app/api/templates/dashboard.html,sha256=6vqMpsAtCBXASHOLnXn3_uZ-U5r7-P3FPo8_NJiMitk,10194
16
- lifx_emulator-3.1.0.dist-info/METADATA,sha256=hOg5mr27_HE83CfaYyjaF_5N4BClqSa3iYQ8kLsr2ec,3225
17
- lifx_emulator-3.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
18
- lifx_emulator-3.1.0.dist-info/entry_points.txt,sha256=tNZHeJTPUXNxu_nuk99ArXLKgwYLhIVVxN7YiaiXBOA,66
19
- lifx_emulator-3.1.0.dist-info/RECORD,,