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> {
|
export async function fetchUIState(): Promise<UIState> {
|
||||||
return fetchLucifer("state");
|
return fetchLucifer("state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import type Device from "$lib/models/device";
|
import type Device from "$lib/models/device";
|
||||||
import { SupportFlags } from "$lib/models/device";
|
import { SupportFlags } from "$lib/models/device";
|
||||||
import { rgb, type ColorRGB } from "../models/color";
|
import { rgb, type ColorRGB } from "../models/color";
|
||||||
import Icon from "./Icon.svelte";
|
import DeviceIcon from "./DeviceIcon.svelte";
|
||||||
|
|
||||||
export let device: Device;
|
export let device: Device;
|
||||||
|
|
||||||
@@ -22,18 +22,18 @@
|
|||||||
let deviceTitle: string;
|
let deviceTitle: string;
|
||||||
let iconColor: ColorRGB | null;
|
let iconColor: ColorRGB | null;
|
||||||
let barColor: ColorRGB | null;
|
let barColor: ColorRGB | null;
|
||||||
let barPercentage: number | null;
|
let barFraction: number | null;
|
||||||
let roundboiText: string | null;
|
let roundboiText: string | null;
|
||||||
$: {
|
$: {
|
||||||
// TODO: Fix device.name on the backend
|
// 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) {
|
if (nameAlias != null) {
|
||||||
deviceTitle = nameAlias.slice("lucifer:name:".length);
|
deviceTitle = nameAlias.slice("lucifer:name:".length);
|
||||||
} else {
|
} else {
|
||||||
deviceTitle = "";
|
deviceTitle = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
barPercentage = null;
|
barFraction = null;
|
||||||
barColor = null;
|
barColor = null;
|
||||||
iconColor = null;
|
iconColor = null;
|
||||||
|
|
||||||
@@ -46,29 +46,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sflags & SupportFlags.Color) {
|
if (sflags & SupportFlags.Color) {
|
||||||
iconColor = device.activeColorRgb;
|
iconColor = device.desiredColorRgb;
|
||||||
} else {
|
} else {
|
||||||
iconColor = null;
|
iconColor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sflags & SupportFlags.SensorPresence) {
|
if (sflags & SupportFlags.SensorPresence) {
|
||||||
barColor = rgb(0.5,0.5,0.5);
|
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) {
|
if (sflags & SupportFlags.Temperature) {
|
||||||
barColor = rgb(1.000,0.2,0.2);
|
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.Intensity) {
|
||||||
if (sflags & SupportFlags.Color) {
|
if (sflags & SupportFlags.Color) {
|
||||||
barColor = device.activeColorRgb;
|
barColor = device.desiredColorRgb;
|
||||||
} else {
|
} else {
|
||||||
barColor = rgb(1.000,0.671,0.355);
|
barColor = rgb(1.000,0.671,0.355);
|
||||||
}
|
}
|
||||||
|
|
||||||
barPercentage = device.desiredState.intensity;
|
barFraction = device.desiredState.intensity;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sflags & SupportFlags.SensorTemperature && !!device.sensors.temperature) {
|
if (sflags & SupportFlags.SensorTemperature && !!device.sensors.temperature) {
|
||||||
@@ -101,20 +101,20 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="row-icon">
|
<div class="row-icon">
|
||||||
{#if iconColor != null}
|
{#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}
|
{: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}
|
{#if !!roundboiText}
|
||||||
<div class="roundboi-text">{roundboiText}</div>
|
<div class="roundboi-text">{roundboiText}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Icon>
|
</DeviceIcon>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="title">{displayTitle}</div>
|
<div class="title">{displayTitle}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flatboi">
|
<div class="flatboi">
|
||||||
{#if barColor != null && barPercentage != null}
|
{#if barColor != null && barFraction != null}
|
||||||
<div style="width: {barPercentage*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
|
<div style="width: {barFraction*100}%; background-color: rgb({barColor.red*255},{barColor.green*255},{barColor.blue*255})" class="flatboi2"></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { fetchUIState } from "$lib/client/lucifer";
|
import { fetchUIState } from "$lib/client/lucifer";
|
||||||
import type Device from "$lib/models/device";
|
import type Device from "$lib/models/device";
|
||||||
|
import type { UIStatePatch } from "$lib/models/uistate";
|
||||||
import type UIState from "$lib/models/uistate";
|
import type UIState from "$lib/models/uistate";
|
||||||
import { getContext, onMount, setContext } from "svelte";
|
import { getContext, onMount, setContext } from "svelte";
|
||||||
import { derived, writable, type Readable } from "svelte/store";
|
import { derived, writable, type Readable } from "svelte/store";
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
|
|
||||||
export interface StateContextData {
|
export interface StateContextData {
|
||||||
reload(): Promise<void>
|
reload(): Promise<void>
|
||||||
state: Readable<UIState | null>
|
state: Readable<UIState>
|
||||||
error: Readable<string | null>
|
error: Readable<string | null>
|
||||||
deviceList: Readable<Device[]>
|
deviceList: Readable<Device[]>
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const state = writable<UIState | null>(null);
|
const state = writable<UIState>({
|
||||||
|
assignments: {},
|
||||||
|
devices: {},
|
||||||
|
script: {},
|
||||||
|
});
|
||||||
const error = writable<string | null>(null);
|
const error = writable<string | null>(null);
|
||||||
|
|
||||||
const deviceList = derived(state, state => {
|
const deviceList = derived(state, state => {
|
||||||
@@ -33,6 +38,8 @@
|
|||||||
.sort((a,b) => a.id.localeCompare(b.id));
|
.sort((a,b) => a.id.localeCompare(b.id));
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
error.set(null);
|
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, {
|
setContext<StateContextData>(ctxKey, {
|
||||||
reload,
|
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"
|
import type { ColorFlags, ColorRGB } from "./color"
|
||||||
|
|
||||||
export default interface Device {
|
export default interface Device {
|
||||||
@@ -6,6 +6,7 @@ export default interface Device {
|
|||||||
name: string
|
name: string
|
||||||
hwMetadata: HardwareMetadata | null
|
hwMetadata: HardwareMetadata | null
|
||||||
hwState: HardwareState | null
|
hwState: HardwareState | null
|
||||||
|
desiredColorRgb: ColorRGB | null
|
||||||
activeColorRgb: ColorRGB | null
|
activeColorRgb: ColorRGB | null
|
||||||
desiredState: State,
|
desiredState: State,
|
||||||
aliases: string[],
|
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 Device from "./device";
|
||||||
|
import type Script from "./script";
|
||||||
|
|
||||||
export default interface UIState {
|
export default interface UIState {
|
||||||
devices: {[id: string]: Device}
|
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">
|
<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 Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte";
|
||||||
import Toolbar from "$lib/components/Toolbar.svelte";
|
import Toolbar from "$lib/components/Toolbar.svelte";
|
||||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
@@ -14,18 +14,18 @@ import Lamp, { DARK_COLOR } from "$lib/components/Lamp.svelte";
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="page" style="font-size: 8em">
|
<div class="page" style="font-size: 8em">
|
||||||
<Icon name="generic_ball" brightColor={rgb(0.517,0.537,1.000)} darkColor={DARK_COLOR} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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} />
|
<DeviceIcon name="hue_playbar" brightColor={rgb(1,0,1)} darkColor={DARK_COLOR} />
|
||||||
<Icon name="hue_motionsensor" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
|
<DeviceIcon name="hue_motionsensor" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
|
||||||
<Icon name="hue_lightbulb_e27" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
|
<DeviceIcon 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} />
|
<DeviceIcon 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="hue_lightbulb_gu10" brightColor={rgb(1,1,1)} darkColor={DARK_COLOR} />
|
||||||
</div>
|
</div>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ require (
|
|||||||
github.com/eriklupander/dtls v0.0.0-20190304211642-b36018226359 // indirect
|
github.com/eriklupander/dtls v0.0.0-20190304211642-b36018226359 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/golang/protobuf v1.3.1 // indirect
|
github.com/golang/protobuf v1.3.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/labstack/gommon v0.4.0 // indirect
|
github.com/labstack/gommon v0.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/services/script"
|
"git.aiterp.net/lucifer3/server/services/script"
|
||||||
"git.aiterp.net/lucifer3/server/services/uistate"
|
"git.aiterp.net/lucifer3/server/services/uistate"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +26,11 @@ var zeroUUID = uuid.UUID{
|
|||||||
|
|
||||||
func New(addr string) (lucifer3.Service, error) {
|
func New(addr string) (lucifer3.Service, error) {
|
||||||
svc := &service{}
|
svc := &service{}
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
@@ -38,6 +45,28 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
return c.JSON(200, data)
|
return c.JSON(200, data)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
e.GET("/subscribe", func(c echo.Context) error {
|
||||||
|
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := make(chan uistate.Patch, 64)
|
||||||
|
svc.addSub(sub)
|
||||||
|
|
||||||
|
defer svc.removeSub(sub)
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
for patch := range sub {
|
||||||
|
err := ws.WriteJSON(patch)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
e.POST("/command", func(c echo.Context) error {
|
e.POST("/command", func(c echo.Context) error {
|
||||||
var input commandInput
|
var input commandInput
|
||||||
err := c.Bind(&input)
|
err := c.Bind(&input)
|
||||||
@@ -115,6 +144,7 @@ type service struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
data uistate.Data
|
data uistate.Data
|
||||||
bus *lucifer3.EventBus
|
bus *lucifer3.EventBus
|
||||||
|
subs []chan uistate.Patch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Active() bool {
|
func (s *service) Active() bool {
|
||||||
@@ -130,12 +160,36 @@ func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|||||||
case uistate.Patch:
|
case uistate.Patch:
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.data = s.data.WithPatch(event)
|
s.data = s.data.WithPatch(event)
|
||||||
|
subs := s.subs
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
// TODO: Broadcast websockets
|
for _, sub := range subs {
|
||||||
|
select {
|
||||||
|
case sub <- event:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *service) addSub(ch chan uistate.Patch) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.subs = append(s.subs, ch)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) removeSub(ch chan uistate.Patch) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.subs = append([]chan uistate.Patch{}, s.subs...)
|
||||||
|
for i, sub := range s.subs {
|
||||||
|
if sub == ch {
|
||||||
|
s.subs = append(s.subs[:i], s.subs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
type commandInput struct {
|
type commandInput struct {
|
||||||
Assign *assignInput `json:"assign,omitempty"`
|
Assign *assignInput `json:"assign,omitempty"`
|
||||||
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
|
AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func (b *Bridge) RefreshAll() ([]lucifer3.Event, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Type == "device" {
|
if res.Type == "device" && !strings.HasPrefix(res.Metadata.Archetype, "bridge") {
|
||||||
hwState, hwEvent := res.GenerateEvent(b.host, resources)
|
hwState, hwEvent := res.GenerateEvent(b.host, resources)
|
||||||
if hwState.SupportFlags == 0 {
|
if hwState.SupportFlags == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -219,7 +219,7 @@ func (b *Bridge) ApplyPatches(date time.Time, resources []ResourceData) (eventLi
|
|||||||
|
|
||||||
for _, resource := range resources {
|
for _, resource := range resources {
|
||||||
if resource.Owner != nil && resource.Owner.Kind == "device" {
|
if resource.Owner != nil && resource.Owner.Kind == "device" {
|
||||||
if parent, ok := mapCopy[resource.Owner.ID]; ok {
|
if parent, ok := mapCopy[resource.Owner.ID]; ok && !strings.HasPrefix(parent.Metadata.Archetype, "bridge") {
|
||||||
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
|
hwState, _ := parent.GenerateEvent(b.host, mapCopy)
|
||||||
eventList = append(eventList, hwState)
|
eventList = append(eventList, hwState)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
|||||||
gentools.ApplyUpdatePtr(&pd.DesiredState, patch.Device.DesiredState)
|
gentools.ApplyUpdatePtr(&pd.DesiredState, patch.Device.DesiredState)
|
||||||
gentools.ApplyUpdatePtr(&pd.Assignment, patch.Device.Assignment)
|
gentools.ApplyUpdatePtr(&pd.Assignment, patch.Device.Assignment)
|
||||||
gentools.ApplyUpdatePtr(&pd.ActiveColorRGB, patch.Device.ActiveColorRGB)
|
gentools.ApplyUpdatePtr(&pd.ActiveColorRGB, patch.Device.ActiveColorRGB)
|
||||||
|
gentools.ApplyUpdatePtr(&pd.DesiredColorRGB, patch.Device.DesiredColorRGB)
|
||||||
|
|
||||||
if patch.Device.AddAlias != nil {
|
if patch.Device.AddAlias != nil {
|
||||||
pd.Aliases = append(pd.Aliases[:0:0], pd.Aliases...)
|
pd.Aliases = append(pd.Aliases[:0:0], pd.Aliases...)
|
||||||
@@ -125,15 +126,16 @@ func (d *Data) ensureAssignment(id uuid.UUID) Assignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HWMetadata *events.HardwareMetadata `json:"hwMetadata"`
|
HWMetadata *events.HardwareMetadata `json:"hwMetadata"`
|
||||||
HWState *events.HardwareState `json:"hwState"`
|
HWState *events.HardwareState `json:"hwState"`
|
||||||
ActiveColorRGB *color.RGB `json:"activeColorRgb"`
|
DesiredColorRGB *color.RGB `json:"desiredColorRgb"`
|
||||||
DesiredState *device.State `json:"desiredState"`
|
ActiveColorRGB *color.RGB `json:"activeColorRgb"`
|
||||||
Aliases []string `json:"aliases"`
|
DesiredState *device.State `json:"desiredState"`
|
||||||
Assignment *uuid.UUID `json:"assignment"`
|
Aliases []string `json:"aliases"`
|
||||||
Sensors DeviceSensors `json:"sensors"`
|
Assignment *uuid.UUID `json:"assignment"`
|
||||||
|
Sensors DeviceSensors `json:"sensors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceSensors struct {
|
type DeviceSensors struct {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ func (e Patch) EventDescription() string {
|
|||||||
case e.Device.ActiveColorRGB != nil:
|
case e.Device.ActiveColorRGB != nil:
|
||||||
col := color.Color{RGB: e.Device.ActiveColorRGB}
|
col := color.Color{RGB: e.Device.ActiveColorRGB}
|
||||||
return fmt.Sprintf("uistate.Patch(device=%s, activeColorRgb=%s)", e.Device.ID, col.String())
|
return fmt.Sprintf("uistate.Patch(device=%s, activeColorRgb=%s)", e.Device.ID, col.String())
|
||||||
|
case e.Device.DesiredColorRGB != nil:
|
||||||
|
col := color.Color{RGB: e.Device.DesiredColorRGB}
|
||||||
|
return fmt.Sprintf("uistate.Patch(device=%s, desiredColorRgb=%s)", e.Device.ID, col.String())
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("uistate.Patch(device=%s, ...other)", e.Device.ID)
|
return fmt.Sprintf("uistate.Patch(device=%s, ...other)", e.Device.ID)
|
||||||
}
|
}
|
||||||
@@ -61,6 +64,7 @@ type DevicePatch struct {
|
|||||||
RemoveAlias *string `json:"removeAlias,omitempty"`
|
RemoveAlias *string `json:"removeAlias,omitempty"`
|
||||||
Assignment *uuid.UUID `json:"assignment,omitempty"`
|
Assignment *uuid.UUID `json:"assignment,omitempty"`
|
||||||
ClearAssignment bool `json:"clearAssignment,omitempty"`
|
ClearAssignment bool `json:"clearAssignment,omitempty"`
|
||||||
|
DesiredColorRGB *color.RGB `json:"desiredColorRgb,omitempty"`
|
||||||
ActiveColorRGB *color.RGB `json:"activeColorRgb,omitempty"`
|
ActiveColorRGB *color.RGB `json:"activeColorRgb,omitempty"`
|
||||||
ClearActiveColorRGB bool `json:"clearActiveColorRGB,omitempty"`
|
ClearActiveColorRGB bool `json:"clearActiveColorRGB,omitempty"`
|
||||||
Sensors *DeviceSensors `json:"sensors,omitempty"`
|
Sensors *DeviceSensors `json:"sensors,omitempty"`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"git.aiterp.net/lucifer3/server/commands"
|
"git.aiterp.net/lucifer3/server/commands"
|
||||||
"git.aiterp.net/lucifer3/server/effects"
|
"git.aiterp.net/lucifer3/server/effects"
|
||||||
"git.aiterp.net/lucifer3/server/events"
|
"git.aiterp.net/lucifer3/server/events"
|
||||||
|
"git.aiterp.net/lucifer3/server/internal/color"
|
||||||
"git.aiterp.net/lucifer3/server/internal/gentools"
|
"git.aiterp.net/lucifer3/server/internal/gentools"
|
||||||
"git.aiterp.net/lucifer3/server/services/script"
|
"git.aiterp.net/lucifer3/server/services/script"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -36,11 +37,29 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command
|
|||||||
|
|
||||||
switch command := command.(type) {
|
switch command := command.(type) {
|
||||||
case commands.SetState:
|
case commands.SetState:
|
||||||
patches = []Patch{{Device: &DevicePatch{ID: command.ID, DesiredState: &command.State}}}
|
rgb := color.Color{}
|
||||||
|
if command.State.Color != nil {
|
||||||
|
rgb, _ = command.State.Color.ToRGB()
|
||||||
|
}
|
||||||
|
|
||||||
|
patches = []Patch{{Device: &DevicePatch{
|
||||||
|
ID: command.ID,
|
||||||
|
DesiredState: &command.State,
|
||||||
|
DesiredColorRGB: rgb.RGB,
|
||||||
|
}}}
|
||||||
case commands.SetStateBatch:
|
case commands.SetStateBatch:
|
||||||
for id, state := range command {
|
for id, state := range command {
|
||||||
|
rgb := color.Color{}
|
||||||
|
if state.Color != nil {
|
||||||
|
rgb, _ = state.Color.ToRGB()
|
||||||
|
}
|
||||||
|
|
||||||
patches = append(patches, Patch{
|
patches = append(patches, Patch{
|
||||||
Device: &DevicePatch{ID: id, DesiredState: gentools.ShallowCopy(&state)},
|
Device: &DevicePatch{
|
||||||
|
ID: id,
|
||||||
|
DesiredState: gentools.ShallowCopy(&state),
|
||||||
|
DesiredColorRGB: rgb.RGB,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case script.Update:
|
case script.Update:
|
||||||
@@ -129,10 +148,13 @@ func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|||||||
}})
|
}})
|
||||||
case events.AssignmentVariables:
|
case events.AssignmentVariables:
|
||||||
// Always set the assignment
|
// Always set the assignment
|
||||||
patches = append(patches, Patch{Assignment: &AssignmentPatch{
|
if len(event.Map) > 0 {
|
||||||
ID: event.ID,
|
patches = append(patches, Patch{Assignment: &AssignmentPatch{
|
||||||
Variables: event.Map,
|
ID: event.ID,
|
||||||
}})
|
Variables: event.Map,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(patches) > 0 {
|
if len(patches) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user