You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

240 lines
6.9 KiB

<script lang="ts" context="module">
import { fetchUIState } from "$lib/client/lucifer";
import type { IconName } from "$lib/components/DeviceIcon.svelte";
import type Device from "$lib/models/device";
import type { UIStatePatch } from "$lib/models/uistate";
import type UIState from "$lib/models/uistate";
import { getContext, onMount, setContext } from "svelte";
import { derived, writable, type Readable } from "svelte/store";
const ctxKey = {ctx: "StateContext"};
export interface StateContextData {
reload(): Promise<void>
state: Readable<UIState>
error: Readable<string | null>
deviceList: Readable<Device[]>
roomList: Readable<{name: string, devices: Device[]}[]>
}
export function getStateContext(): StateContextData {
return getContext(ctxKey) as StateContextData;
}
</script>
<script lang="ts">
const state = writable<UIState>({
assignments: {},
devices: {},
scripts: {},
});
const error = writable<string | null>(null);
let socket: WebSocket | null = null;
const deviceList = derived(state, state => {
if (state == null) {
return [];
}
return Object.keys(state.devices)
.map(k => state.devices[k])
.sort((a,b) => a.name.localeCompare(b.name));
});
const roomList = derived(state, state => {
const roomMap: Record<string, Device[]> = {};
for (const id in state.devices) {
if (!state.devices.hasOwnProperty(id)) {
continue;
}
const roomAlias = state.devices[id].aliases.find(a => a.startsWith("lucifer:room:"));
const roomName = roomAlias?.slice("lucifer:room:".length) || "Unroomed";
if (!roomMap[roomName]) {
roomMap[roomName] = [];
}
roomMap[roomName].push(state.devices[id]);
}
return Object.keys(roomMap)
.map(k => ({ name: k, devices: roomMap[k] }))
.sort((a,b) => a.name.localeCompare(b.name));
});
async function reload() {
error.set(null);
try {
const newState = await fetchUIState();
state.set(newState);
} catch(err) {
error.set(err?.toString ? err.toString() : String(err))
}
}
async function connectSocket() {
let url = `ws://${window.location.host}/subscribe`;
if (import.meta.env.VITE_LUCIFER4_BACKEND_URL != null) {
url = import.meta.env.VITE_LUCIFER4_BACKEND_URL.replace("http", "ws") +"/subscribe";
}
const currSocket = new WebSocket(url);
currSocket.onmessage = (msg) => {
const patch: UIStatePatch = JSON.parse(msg.data);
state.update(s => {
if (patch.device) {
if (patch.device.delete) {
const devices = {...s.devices};
delete devices[patch.device.id];
return {...s, devices};
} else {
return {
...s,
devices: {
...s.devices,
[patch.device.id]: ((patch) => {
if (patch.addAlias) {
const aliases = [...(s.devices[patch.id].aliases || [])];
const exclPrefix = ["lucifer:icon:", "lucifer:group:", "lucifer:name:"].find(p => patch.addAlias?.startsWith(p));
const exclExisting = exclPrefix && aliases.find(a => a.startsWith(exclPrefix));
if (patch.addAlias.startsWith("lucifer:name:")) {
patch.name = patch.addAlias.slice("lucifer:name:".length)
}
if (patch.addAlias.startsWith("lucifer:icon:")) {
patch.icon = patch.addAlias.slice("lucifer:icon:".length) as IconName
}
if (exclExisting) {
return {
...s.devices[patch.id],
aliases: [...aliases.filter(a => a != exclExisting), patch.addAlias].sort(),
name: patch.name || s.devices[patch.id].name,
icon: patch.icon || s.devices[patch.id].icon,
};
} else {
return {
...s.devices[patch.id],
aliases: [...aliases, patch.addAlias].sort(),
name: patch.name || s.devices[patch.id].name,
icon: patch.icon || s.devices[patch.id].icon,
};
}
} else if (patch.removeAlias != null) {
const aliases = [...(s.devices[patch.id].aliases || [])];
return {
...s.devices[patch.id],
aliases: aliases.filter(a => a !== patch.removeAlias),
};
} else {
return {
...s.devices[patch.id],
...patch,
};
}
})(patch.device)
}
}
}
}
if (patch.assignment) {
if (patch.assignment.delete) {
const assignments = {...s.assignments};
delete assignments[patch.assignment.id];
return {...s, assignments};
} else {
return {
...s,
assignments: {
...s.assignments,
[patch.assignment.id]: {
...s.assignments[patch.assignment.id],
...patch.assignment,
}
}
}
}
}
if (patch.script) {
if (patch.script.delete) {
const scripts = {...s.scripts};
delete scripts[patch.script.id];
return {...s, scripts};
} else {
return {
...s,
scripts: {
...s.scripts,
[patch.script.id]: {
...s.scripts[patch.script.id],
...patch.script,
}
}
}
}
}
return s;
})
}
currSocket.onopen = () => {
if (socket !== null) {
socket.close();
}
socket = currSocket;
}
currSocket.onerror = err => {
console.warn("Socket failed:", err);
currSocket.close();
setTimeout(() => connectSocket(), 3000);
}
currSocket.onclose = () => {
if (currSocket === socket) {
socket = null;
}
}
}
onMount(() => {
reload();
const interval = setInterval(reload, 60000);
connectSocket();
window.addEventListener("visibilitychange", () => {
if (document.visibilityState == "visible") {
console.log("Reconnecting");
reload();
connectSocket();
} else {
if (socket != null) {
console.log("Disconnecting")
socket.close();
}
}
});
return () => clearInterval(interval);
});
setContext<StateContextData>(ctxKey, {
reload,
error: { subscribe: error.subscribe },
state: { subscribe: state.subscribe },
deviceList,
roomList,
});
</script>
<slot></slot>