websocket backend + frontend patching.

This commit is contained in:
2023-08-30 22:56:58 +02:00
parent e8b77ae69c
commit 64bb737569
17 changed files with 664 additions and 361 deletions
+369 -312
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,3 +21,4 @@ export default async function fetchLucifer<T>(path: string, init?: RequestInit):
export async function fetchUIState(): Promise<UIState> {
return fetchLucifer("state");
}
+14 -14
View File
@@ -8,7 +8,7 @@
import type Device from "$lib/models/device";
import { SupportFlags } from "$lib/models/device";
import { rgb, type ColorRGB } from "../models/color";
import Icon from "./Icon.svelte";
import DeviceIcon from "./DeviceIcon.svelte";
export let device: Device;
@@ -22,18 +22,18 @@
let deviceTitle: string;
let iconColor: ColorRGB | null;
let barColor: ColorRGB | null;
let barPercentage: number | null;
let barFraction: number | null;
let roundboiText: string | null;
$: {
// TODO: Fix device.name on the backend
const nameAlias = device.aliases.find(a => a.startsWith("lucifer:name:"));
const nameAlias = device.aliases?.find(a => a.startsWith("lucifer:name:"));
if (nameAlias != null) {
deviceTitle = nameAlias.slice("lucifer:name:".length);
} else {
deviceTitle = "";
}
barPercentage = null;
barFraction = null;
barColor = null;
iconColor = null;
@@ -46,29 +46,29 @@
}
if (sflags & SupportFlags.Color) {
iconColor = device.activeColorRgb;
iconColor = device.desiredColorRgb;
} else {
iconColor = null;
}
if (sflags & SupportFlags.SensorPresence) {
barColor = rgb(0.5,0.5,0.5);
barPercentage = Math.max(0, (300 - (device.sensors.lastMotion||300))/300) * 100;
barFraction = Math.max(0, (300 - (device.sensors.lastMotion!=null?device.sensors.lastMotion:300))/300);
}
if (sflags & SupportFlags.Temperature) {
barColor = rgb(1.000,0.2,0.2);
barPercentage = Math.min(1, Math.max(0, (device.desiredState.temperature||0) - 5) / 35) * 100;
barFraction = Math.min(1, Math.max(0, (device.desiredState.temperature||0) - 5) / 35) * 1;
}
if (sflags & SupportFlags.Intensity) {
if (sflags & SupportFlags.Color) {
barColor = device.activeColorRgb;
barColor = device.desiredColorRgb;
} else {
barColor = rgb(1.000,0.671,0.355);
}
barPercentage = device.desiredState.intensity;
barFraction = device.desiredState.intensity;
}
if (sflags & SupportFlags.SensorTemperature && !!device.sensors.temperature) {
@@ -101,20 +101,20 @@
<div class="row">
<div class="row-icon">
{#if iconColor != null}
<Icon name={device.hwMetadata?.icon||"generic_ball"} brightColor={iconColor} darkColor={darkColor} />
<DeviceIcon name={device.hwMetadata?.icon||"generic_ball"} brightColor={iconColor} darkColor={darkColor} />
{:else}
<Icon name={device.hwMetadata?.icon||"generic_ball"} brightColor={darkColor} darkColor={darkColor}>
<DeviceIcon name={device.hwMetadata?.icon||"generic_ball"} brightColor={darkColor} darkColor={darkColor}>
{#if !!roundboiText}
<div class="roundboi-text">{roundboiText}</div>
{/if}
</Icon>
</DeviceIcon>
{/if}
</div>
<div class="title">{displayTitle}</div>
</div>
<div class="flatboi">
{#if barColor != null && barPercentage != null}
<div style="width: {barPercentage*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
{#if barColor != null && barFraction != null}
<div style="width: {barFraction*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
{/if}
</div>
</div>
+100 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts" context="module">
import { fetchUIState } from "$lib/client/lucifer";
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";
@@ -9,7 +10,7 @@
export interface StateContextData {
reload(): Promise<void>
state: Readable<UIState | null>
state: Readable<UIState>
error: Readable<string | null>
deviceList: Readable<Device[]>
}
@@ -20,7 +21,11 @@
</script>
<script lang="ts">
const state = writable<UIState | null>(null);
const state = writable<UIState>({
assignments: {},
devices: {},
script: {},
});
const error = writable<string | null>(null);
const deviceList = derived(state, state => {
@@ -33,6 +38,8 @@
.sort((a,b) => a.id.localeCompare(b.id));
})
async function reload() {
error.set(null);
@@ -44,7 +51,97 @@
}
}
onMount(() => { reload(); });
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 socket = new WebSocket(url);
socket.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]: {
...s.devices[patch.device.id],
...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;
})
}
socket.onerror = err => {
console.warn("Socket failed:", err);
socket.close();
}
socket.onclose = () => {
setTimeout(() => connectSocket(), 3000);
}
}
onMount(() => {
reload();
const interval = setInterval(reload, 60000);
connectSocket();
return () => clearInterval(interval);
});
setContext<StateContextData>(ctxKey, {
reload,
+15
View File
@@ -0,0 +1,15 @@
import type { State } from "./device"
export default interface Assignment {
id: string
deviceIds: string[]
effect: Effect
variables: Record<string, number>
}
export type Effect =
| { manual: State }
| { gradient: { states: State[], animationMs: number, reverse: boolean, interpolate: boolean } }
| { pattern: { states: State[], animationMs: number } }
| { random: { states: State[], animationMs: number, interpolate: boolean } }
| { vrange: { states: State[], variable: string, min: number, max: number } }
+2 -1
View File
@@ -1,4 +1,4 @@
import type { IconName } from "$lib/components/Icon.svelte"
import type { IconName } from "$lib/components/DeviceIcon.svelte"
import type { ColorFlags, ColorRGB } from "./color"
export default interface Device {
@@ -6,6 +6,7 @@ export default interface Device {
name: string
hwMetadata: HardwareMetadata | null
hwState: HardwareState | null
desiredColorRgb: ColorRGB | null
activeColorRgb: ColorRGB | null
desiredState: State,
aliases: string[],
+37
View File
@@ -0,0 +1,37 @@
import type { Effect } from "./assignment"
export default interface Script {
name: string
lines: ScriptLine[]
}
export interface ScriptLine {
if: ScriptLineIf
assign: ScriptLineAssign
set: ScriptLineSet
}
export interface ScriptLineIf {
condition: ScriptCondition
then: ScriptLine[]
else: ScriptLine[]
}
export interface ScriptCondition {
scope: string
key: string
op: string
value?: string
not?: boolean
}
export interface ScriptLineAssign {
match: string
effect: Effect
}
export interface ScriptLineSet {
scope: string
key: string
value: string
}
+10
View File
@@ -1,5 +1,15 @@
import type Assignment from "./assignment";
import type Device from "./device";
import type Script from "./script";
export default interface UIState {
devices: {[id: string]: Device}
assignments: {[id: string]: Assignment}
scripts: {[id: string]: Script}
}
export interface UIStatePatch {
device: Partial<Device> & { id: string, delete?: boolean }
assignment: Partial<Assignment> & { id: string, delete?: boolean }
script: Partial<Script> & { id: string, delete?: boolean }
}
+13 -13
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "$lib/components/Icon.svelte";
import DeviceIcon from "$lib/components/DeviceIcon.svelte";
import Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte";
import Toolbar from "$lib/components/Toolbar.svelte";
import { getStateContext } from "$lib/contexts/StateContext.svelte";
@@ -14,18 +14,18 @@ import Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte";
{/each}
</div>
<div class="page" style="font-size: 8em">
<Icon name="generic_ball" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<Icon name="generic_boob" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<Icon name="generic_lamp" brightColor={rgb(0.667,0.750,1.000)} darkColor={DARK_COLOR} />
<Icon name="generic_strip" brightColor={rgb(0.667,0.750,1.000)} darkColor={DARK_COLOR} />
<Icon name="hue_adore_tube" brightColor={rgb(1,1,0)} darkColor={DARK_COLOR} />
<Icon name="hue_signe" brightColor={rgb(0.300,1.000,0.300)} darkColor={DARK_COLOR} />
<Icon name="hue_dimmerswitch" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<Icon name="hue_playbar" brightColor={rgb(1,0,1)} darkColor={DARK_COLOR} />
<Icon name="hue_motionsensor" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<Icon name="hue_lightbulb_e27" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<Icon name="hue_lightbulb_e14" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<Icon name="hue_lightbulb_gu10" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<DeviceIcon name="generic_ball" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<DeviceIcon name="generic_boob" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<DeviceIcon name="generic_lamp" brightColor={rgb(0.667,0.750,1.000)} darkColor={DARK_COLOR} />
<DeviceIcon name="generic_strip" brightColor={rgb(0.667,0.750,1.000)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_adore_tube" brightColor={rgb(1,1,0)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_signe" brightColor={rgb(0.300,1.000,0.300)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_dimmerswitch" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_playbar" brightColor={rgb(1,0,1)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_motionsensor" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_lightbulb_e27" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_lightbulb_e14" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
<DeviceIcon name="hue_lightbulb_gu10" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
</div>
<Toolbar />