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.
- {lifx_emulator-3.1.0.dist-info → lifx_emulator-4.2.0.dist-info}/METADATA +2 -1
- lifx_emulator-4.2.0.dist-info/RECORD +43 -0
- lifx_emulator_app/__main__.py +693 -137
- lifx_emulator_app/api/__init__.py +0 -4
- lifx_emulator_app/api/app.py +122 -16
- lifx_emulator_app/api/models.py +32 -1
- lifx_emulator_app/api/routers/__init__.py +5 -1
- lifx_emulator_app/api/routers/devices.py +64 -10
- lifx_emulator_app/api/routers/products.py +42 -0
- lifx_emulator_app/api/routers/scenarios.py +55 -52
- lifx_emulator_app/api/routers/websocket.py +70 -0
- lifx_emulator_app/api/services/__init__.py +21 -4
- lifx_emulator_app/api/services/device_service.py +188 -1
- lifx_emulator_app/api/services/event_bridge.py +234 -0
- lifx_emulator_app/api/services/scenario_service.py +153 -0
- lifx_emulator_app/api/services/websocket_manager.py +326 -0
- lifx_emulator_app/api/static/_app/env.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/assets/0.DOQLX7EM.css +1 -0
- lifx_emulator_app/api/static/_app/immutable/assets/2.CU0O2Xrb.css +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/BORyfda6.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/BTLkiQR5.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/BaoxLdOF.js +2 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/Binc8JbE.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/CDSQEL5N.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/DfIkQq0Y.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/MAGDeS2Z.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/N3z8axFy.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/chunks/yhjkpkcN.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/entry/app.Dhwm664s.js +2 -0
- lifx_emulator_app/api/static/_app/immutable/entry/start.Nqz6UJJT.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/nodes/0.CPncm6RP.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/nodes/1.x-f3libw.js +1 -0
- lifx_emulator_app/api/static/_app/immutable/nodes/2.BP5Yvqf4.js +6 -0
- lifx_emulator_app/api/static/_app/version.json +1 -0
- lifx_emulator_app/api/static/index.html +38 -0
- lifx_emulator_app/api/static/robots.txt +3 -0
- lifx_emulator_app/config.py +316 -0
- lifx_emulator-3.1.0.dist-info/RECORD +0 -19
- lifx_emulator_app/api/static/dashboard.js +0 -588
- lifx_emulator_app/api/templates/dashboard.html +0 -357
- {lifx_emulator-3.1.0.dist-info → lifx_emulator-4.2.0.dist-info}/WHEEL +0 -0
- {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,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,,
|