websocket backend + frontend patching.
This commit is contained in:
Generated
+369
-312
File diff suppressed because it is too large
Load Diff
@@ -21,3 +21,4 @@ export default async function fetchLucifer<T>(path: string, init?: RequestInit):
|
||||
export async function fetchUIState(): Promise<UIState> {
|
||||
return fetchLucifer("state");
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } }
|
||||
@@ -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[],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user