trigger editor
This commit is contained in:
@@ -13,6 +13,6 @@
|
|||||||
&.reverse
|
&.reverse
|
||||||
flex-direction: row-reverse
|
flex-direction: row-reverse
|
||||||
|
|
||||||
@media screen and (max-width: 500px)
|
@media screen and (max-width: 700px)
|
||||||
display: block
|
display: block
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
|
import { getModalContext, type ModalSelection } from "$lib/contexts/ModalContext.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
export let show: boolean = false;
|
export let show: boolean = false;
|
||||||
export let titleText: string = "Edit Form";
|
export let titleText: string = "Edit Form";
|
||||||
@@ -204,6 +205,9 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
padding: 0.5em 1ch;
|
padding: 0.5em 1ch;
|
||||||
}
|
}
|
||||||
|
div.modal :global(input[type="time"]:not(.custom)) {
|
||||||
|
padding: 0.333em 1ch;
|
||||||
|
}
|
||||||
div.modal :global(select:not(.custom)) {
|
div.modal :global(select:not(.custom)) {
|
||||||
padding-left: 0.5ch;
|
padding-left: 0.5ch;
|
||||||
padding: 0.45em 1ch;
|
padding: 0.45em 1ch;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
| { kind: "closed" }
|
| { kind: "closed" }
|
||||||
| { kind: "device.edit", op: DeviceEditOp }
|
| { kind: "device.edit", op: DeviceEditOp }
|
||||||
| { kind: "script.edit" }
|
| { kind: "script.edit" }
|
||||||
|
| { kind: "trigger.edit" }
|
||||||
|
|
||||||
export interface ModalContextState {
|
export interface ModalContextState {
|
||||||
modal: Writable<ModalSelection>
|
modal: Writable<ModalSelection>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
deviceList: Readable<Device[]>
|
deviceList: Readable<Device[]>
|
||||||
assignmentList: Readable<Assignment[]>
|
assignmentList: Readable<Assignment[]>
|
||||||
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
|
roomList: Readable<{name: string, groups: {name: string, devices: Device[]}[], devices: Device[]}[]>
|
||||||
|
maskList: Readable<string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStateContext(): StateContextData {
|
export function getStateContext(): StateContextData {
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
assignments: {},
|
assignments: {},
|
||||||
devices: {},
|
devices: {},
|
||||||
scripts: {},
|
scripts: {},
|
||||||
|
triggers: {},
|
||||||
});
|
});
|
||||||
const error = writable<string | null>(null);
|
const error = writable<string | null>(null);
|
||||||
|
|
||||||
@@ -45,6 +47,13 @@
|
|||||||
.sort((a,b) => a.name.localeCompare(b.name));
|
.sort((a,b) => a.name.localeCompare(b.name));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const maskList = derived(state, state => {
|
||||||
|
return Object.keys(state.devices)
|
||||||
|
.flatMap(k => state.devices[k].aliases.filter(a => ["tag", "room", "role", "name"].find(p => a.startsWith(`lucifer:${p}:`))))
|
||||||
|
.sort()
|
||||||
|
.filter((e, i, a) => a[i - 1] !== e)
|
||||||
|
});
|
||||||
|
|
||||||
const assignmentList = derived(state, state => {
|
const assignmentList = derived(state, state => {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return [];
|
return [];
|
||||||
@@ -55,7 +64,6 @@
|
|||||||
.sort((a,b) => a.id.localeCompare(b.id));
|
.sort((a,b) => a.id.localeCompare(b.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const roomList = derived(state, state => {
|
const roomList = derived(state, state => {
|
||||||
const roomMap: Record<string, Device[]> = {};
|
const roomMap: Record<string, Device[]> = {};
|
||||||
const groupMap: Record<string, string> = {};
|
const groupMap: Record<string, string> = {};
|
||||||
@@ -132,8 +140,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.devices = {...state.devices}
|
state.devices = {...state.devices}
|
||||||
if (patch.devices[deviceId] != null) {
|
if (patch.devices[deviceId] !== null) {
|
||||||
state.devices[deviceId] = patch.devices[deviceId]
|
state.devices[deviceId] = patch.devices[deviceId]!
|
||||||
} else {
|
} else {
|
||||||
delete state.devices[deviceId]
|
delete state.devices[deviceId]
|
||||||
}
|
}
|
||||||
@@ -146,7 +154,7 @@
|
|||||||
|
|
||||||
state.assignments = {...state.assignments}
|
state.assignments = {...state.assignments}
|
||||||
if (patch.assignments[assignmentId] != null) {
|
if (patch.assignments[assignmentId] != null) {
|
||||||
state.assignments[assignmentId] = patch.assignments[assignmentId]
|
state.assignments[assignmentId] = patch.assignments[assignmentId]!
|
||||||
} else {
|
} else {
|
||||||
delete state.assignments[assignmentId]
|
delete state.assignments[assignmentId]
|
||||||
}
|
}
|
||||||
@@ -159,12 +167,25 @@
|
|||||||
|
|
||||||
state.scripts = {...state.scripts}
|
state.scripts = {...state.scripts}
|
||||||
if (patch.scripts[scriptName] != null) {
|
if (patch.scripts[scriptName] != null) {
|
||||||
state.scripts[scriptName] = patch.scripts[scriptName]
|
state.scripts[scriptName] = patch.scripts[scriptName]!
|
||||||
} else {
|
} else {
|
||||||
delete state.scripts[scriptName]
|
delete state.scripts[scriptName]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const triggerId in patch.triggers) {
|
||||||
|
if (!patch.triggers.hasOwnProperty(triggerId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
state.triggers = {...state.triggers}
|
||||||
|
if (patch.triggers[triggerId] != null) {
|
||||||
|
state.triggers[triggerId] = patch.triggers[triggerId]!
|
||||||
|
} else {
|
||||||
|
delete state.triggers[triggerId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -214,6 +235,7 @@
|
|||||||
deviceList,
|
deviceList,
|
||||||
assignmentList,
|
assignmentList,
|
||||||
roomList,
|
roomList,
|
||||||
|
maskList,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { runCommand } from "$lib/client/lucifer";
|
||||||
|
import HSplit from "$lib/components/HSplit.svelte";
|
||||||
|
import HSplitPart from "$lib/components/HSplitPart.svelte";
|
||||||
|
import Modal from "$lib/components/Modal.svelte";
|
||||||
|
import ModalBody from "$lib/components/ModalBody.svelte";
|
||||||
|
import ModalSection from "$lib/components/ModalSection.svelte";
|
||||||
|
import ScriptLineBlock from "$lib/components/scripting/ScriptLineBlock.svelte";
|
||||||
|
import { getModalContext } from "$lib/contexts/ModalContext.svelte";
|
||||||
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
|
import { fromEditableTrigger, newEditableScriptLine, newEditableTrigger, toEditableScriptLine, toEditableTrigger, type TriggerEditable } from "$lib/models/script";
|
||||||
|
|
||||||
|
const { state, deviceList, maskList } = getStateContext();
|
||||||
|
const { modal } = getModalContext();
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
let disabled = false;
|
||||||
|
let selectedTrigger = "";
|
||||||
|
let deleteCount = 3;
|
||||||
|
|
||||||
|
let current: TriggerEditable = newEditableTrigger("*", "");
|
||||||
|
|
||||||
|
async function submitForm() {
|
||||||
|
disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const before = Object.keys($state.triggers);
|
||||||
|
await runCommand({updateTrigger: fromEditableTrigger(current)});
|
||||||
|
|
||||||
|
if (current.id === null) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
const after = Object.keys($state.triggers);
|
||||||
|
selectedTrigger = after.find(id => !before.includes(id)) || triggerList[0].id;
|
||||||
|
}
|
||||||
|
} catch(_) {}
|
||||||
|
|
||||||
|
deleteCount = 3;
|
||||||
|
disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTrigger() {
|
||||||
|
deleteCount -= 1;
|
||||||
|
if (deleteCount <= 0) {
|
||||||
|
try {
|
||||||
|
await runCommand({deleteTrigger: {id: selectedTrigger}});
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
selectedTrigger = triggerList[0]?.id || "";
|
||||||
|
} catch(_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTrigger(id: string) {
|
||||||
|
deleteCount = 3;
|
||||||
|
|
||||||
|
if (id !== "") {
|
||||||
|
current = toEditableTrigger($state.triggers[id]);
|
||||||
|
} else {
|
||||||
|
current = newEditableTrigger($maskList[0], scriptList?.[0] || "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPreLine() {
|
||||||
|
current.scriptPre = [...current.scriptPre, newEditableScriptLine()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPostLine() {
|
||||||
|
current.scriptPost = [...current.scriptPost, newEditableScriptLine()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureButton(buttonList: string[]) {
|
||||||
|
if (!buttonList.includes(current.button)) {
|
||||||
|
current.button = buttonList[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: loadTrigger(selectedTrigger);
|
||||||
|
$: triggerList = Object.keys($state.triggers).map(k => $state.triggers[k]).sort((a, b) => (
|
||||||
|
a.event.localeCompare(b.event)
|
||||||
|
|| a.deviceMatch.localeCompare(b.deviceMatch)
|
||||||
|
|| a.parameter.localeCompare(b.parameter)
|
||||||
|
));
|
||||||
|
$: buttonList = $deviceList.filter(d => d.aliases.includes(current.deviceMatch))
|
||||||
|
.flatMap(d => d.hwState?.buttons||[])
|
||||||
|
.sort()
|
||||||
|
.filter((e,i,a) => a[i-1] !== e);
|
||||||
|
$: scriptList = Object.keys($state.scripts).sort();
|
||||||
|
$: maskGroups = $maskList.map(s => s.split(":")[1]).filter((e, i, a) => a[i-1] !== e).sort().reverse();
|
||||||
|
$: show = ($modal.kind === "trigger.edit");
|
||||||
|
$: if (scriptList.length > 0 && current.scriptName === "") { current.scriptName = scriptList[0]; };
|
||||||
|
$: ensureButton(buttonList);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form novalidate on:submit|preventDefault={submitForm}>
|
||||||
|
<Modal
|
||||||
|
ultrawide closable show={show}
|
||||||
|
titleText="Trigger Editor"
|
||||||
|
disabled={disabled}
|
||||||
|
submitText="Save Trigger"
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<HSplit>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
<label for="mask">Trigger</label>
|
||||||
|
<select bind:value={selectedTrigger}>
|
||||||
|
{#each triggerList as trigger (trigger.id)}
|
||||||
|
<option value={trigger.id}>{trigger.name || `${trigger.deviceMatch} – ${trigger.event} ${trigger.parameter}`}</option>
|
||||||
|
{/each}
|
||||||
|
<option value="">New Trigger</option>
|
||||||
|
</select>
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={2}>
|
||||||
|
</HSplitPart>
|
||||||
|
</HSplit>
|
||||||
|
<HSplit>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
<label for="mask">Event</label>
|
||||||
|
<select bind:value={current.event}>
|
||||||
|
<option value="Button">Button Pressed</option>
|
||||||
|
<option value="Time">Time Changed</option>
|
||||||
|
</select>
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
{#if current.event === "Button"}
|
||||||
|
<label for="mask">Button</label>
|
||||||
|
<select bind:value={current.button}>
|
||||||
|
{#each buttonList as buttonOption}
|
||||||
|
<option value={buttonOption}>{buttonOption}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{:else}
|
||||||
|
<label for="mask">Time</label>
|
||||||
|
<input type="time" bind:value={current.time} />
|
||||||
|
{/if}
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
{#if current.event === "Button"}
|
||||||
|
<label for="mask">Device</label>
|
||||||
|
<select bind:value={current.deviceMatch}>
|
||||||
|
{#each maskGroups as g}
|
||||||
|
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}">
|
||||||
|
{#each $maskList.filter(m => m.split(":")[1] === g) as mask}
|
||||||
|
<option value={mask}>{mask}</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{/if}
|
||||||
|
</HSplitPart>
|
||||||
|
</HSplit>
|
||||||
|
<HSplit>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
<label for="mask">Run Script</label>
|
||||||
|
<select bind:value={current.scriptName}>
|
||||||
|
{#each scriptList as script}
|
||||||
|
<option value={script}>{script}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
<label for="mask">Script Target</label>
|
||||||
|
<select bind:value={current.scriptTarget}>
|
||||||
|
{#each maskGroups as g}
|
||||||
|
<optgroup label="{g.slice(0, 1).toUpperCase() + g.slice(1)}">
|
||||||
|
{#each $maskList.filter(m => m.split(":")[1] === g) as mask}
|
||||||
|
<option value={mask}>{mask}</option>
|
||||||
|
{/each}
|
||||||
|
</optgroup>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</HSplitPart>
|
||||||
|
<HSplitPart weight={1}>
|
||||||
|
</HSplitPart>
|
||||||
|
</HSplit>
|
||||||
|
<ModalSection expanded disableExpandToggle title="Trigger-Specific Scripting">
|
||||||
|
<ScriptLineBlock add on:add={addPreLine} label="Before Script" bind:value={current.scriptPre} />
|
||||||
|
<ScriptLineBlock add on:add={addPostLine} label="After Script" bind:value={current.scriptPost} />
|
||||||
|
</ModalSection>
|
||||||
|
</ModalBody>
|
||||||
|
<button slot="secondary-button-1" disabled={disabled || selectedTrigger === ""} on:click|preventDefault={deleteTrigger}>Delete ({deleteCount})</button>
|
||||||
|
</Modal>
|
||||||
|
</form>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AssignmentInput } from "./assignment"
|
import type { AssignmentInput } from "./assignment"
|
||||||
import type { ScriptLine } from "./script"
|
import type { ScriptLine, TriggerInput } from "./script"
|
||||||
|
|
||||||
export default interface CommandInput {
|
export default interface CommandInput {
|
||||||
addAlias?: AddAliasCommand
|
addAlias?: AddAliasCommand
|
||||||
@@ -7,6 +7,8 @@ export default interface CommandInput {
|
|||||||
assign?: AssignmentInput
|
assign?: AssignmentInput
|
||||||
updateScript?: UpdateScriptCommand
|
updateScript?: UpdateScriptCommand
|
||||||
executeScript?: ExecuteScriptCommand
|
executeScript?: ExecuteScriptCommand
|
||||||
|
updateTrigger?: UpdateTriggerCommand
|
||||||
|
deleteTrigger?: DeleteTriggerCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateScriptCommand {
|
export interface UpdateScriptCommand {
|
||||||
@@ -27,4 +29,10 @@ export interface AddAliasCommand {
|
|||||||
export interface RemoveAliasComamnd {
|
export interface RemoveAliasComamnd {
|
||||||
match: string
|
match: string
|
||||||
alias: string
|
alias: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateTriggerCommand = TriggerInput
|
||||||
|
|
||||||
|
export interface DeleteTriggerCommand {
|
||||||
|
id: string
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,35 @@ export default interface Script {
|
|||||||
lines: ScriptLine[]
|
lines: ScriptLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Trigger {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
event: "Button" | "Time"
|
||||||
|
deviceMatch: string
|
||||||
|
parameter: string
|
||||||
|
scriptTarget: string
|
||||||
|
scriptName: string
|
||||||
|
scriptPre: ScriptLine[]
|
||||||
|
scriptPost: ScriptLine[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriggerInput extends Pick<Trigger, Exclude<keyof Trigger, "id">> {
|
||||||
|
id: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriggerEditable {
|
||||||
|
id: string | null
|
||||||
|
name: string
|
||||||
|
event: "Button" | "Time"
|
||||||
|
deviceMatch: string
|
||||||
|
time: string
|
||||||
|
button: string
|
||||||
|
scriptTarget: string
|
||||||
|
scriptName: string
|
||||||
|
scriptPre: ScriptLineEditable[]
|
||||||
|
scriptPost: ScriptLineEditable[]
|
||||||
|
}
|
||||||
|
|
||||||
type ScriptLineEditableData = ScriptLineIf & ScriptLineAssign & ScriptLineSet & ScriptLineSelect
|
type ScriptLineEditableData = ScriptLineIf & ScriptLineAssign & ScriptLineSet & ScriptLineSelect
|
||||||
|
|
||||||
export interface ScriptLineEditable {
|
export interface ScriptLineEditable {
|
||||||
@@ -92,6 +121,14 @@ export function fromEditableScriptLine(line: ScriptLineEditable): ScriptLine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function newEditableScriptLine(): ScriptLineEditable {
|
||||||
|
return toEditableScriptLine({if: {
|
||||||
|
condition: { scope: "global", key: "", op: "eq" },
|
||||||
|
then: [],
|
||||||
|
else: [],
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
export function toEditableScriptLine(line: ScriptLine): ScriptLineEditable {
|
export function toEditableScriptLine(line: ScriptLine): ScriptLineEditable {
|
||||||
const base = emptyEditable();
|
const base = emptyEditable();
|
||||||
|
|
||||||
@@ -170,4 +207,48 @@ function emptyEditable(): ScriptLineEditable {
|
|||||||
then: [],
|
then: [],
|
||||||
else: [],
|
else: [],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newEditableTrigger(deviceMatch: string, scriptName: string): TriggerEditable {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
event: "Button",
|
||||||
|
deviceMatch: deviceMatch,
|
||||||
|
button: "On",
|
||||||
|
time: "00:00",
|
||||||
|
scriptName: scriptName,
|
||||||
|
scriptTarget: deviceMatch,
|
||||||
|
scriptPre: [],
|
||||||
|
scriptPost: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toEditableTrigger(trigger: Trigger): TriggerEditable {
|
||||||
|
return {
|
||||||
|
id: trigger.id,
|
||||||
|
name: trigger.name,
|
||||||
|
event: trigger.event,
|
||||||
|
deviceMatch: trigger.deviceMatch,
|
||||||
|
button: trigger.event === "Button" ? trigger.parameter : "Up",
|
||||||
|
time: trigger.event === "Time" ? trigger.parameter : "00:00",
|
||||||
|
scriptName: trigger.scriptName,
|
||||||
|
scriptTarget: trigger.scriptTarget,
|
||||||
|
scriptPre: trigger.scriptPre.map(toEditableScriptLine),
|
||||||
|
scriptPost: trigger.scriptPost.map(toEditableScriptLine),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromEditableTrigger(editable: TriggerEditable): TriggerInput {
|
||||||
|
return {
|
||||||
|
id: editable.id || undefined,
|
||||||
|
name: editable.name,
|
||||||
|
event: editable.event,
|
||||||
|
deviceMatch: editable.deviceMatch,
|
||||||
|
parameter: (editable.event === "Button") ? editable.button : editable.time,
|
||||||
|
scriptTarget: editable.scriptTarget,
|
||||||
|
scriptName: editable.scriptName,
|
||||||
|
scriptPre: editable.scriptPre.map(fromEditableScriptLine),
|
||||||
|
scriptPost: editable.scriptPost.map(fromEditableScriptLine),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,26 @@
|
|||||||
import type Assignment from "./assignment";
|
import type Assignment from "./assignment";
|
||||||
import type Device from "./device";
|
import type Device from "./device";
|
||||||
import type { ScriptLine } from "./script";
|
import type { ScriptLine, Trigger } from "./script";
|
||||||
import type Script from "./script";
|
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}
|
assignments: {[id: string]: Assignment}
|
||||||
scripts: {[id: string]: ScriptLine[]}
|
scripts: {[id: string]: ScriptLine[]}
|
||||||
|
triggers: {[id: string]: Trigger}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UIStatePatch {
|
export interface UIStatePatch {
|
||||||
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string }
|
device: Partial<Device> & { id: string, delete?: boolean, addAlias?: string, removeAlias?: string }
|
||||||
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string }
|
assignment: Partial<Assignment> & { id: string, delete?: boolean, addDeviceId?: string, removeDeviceId?: string }
|
||||||
script: Partial<Script> & { id: string, delete?: boolean }
|
script: Partial<Script> & { id: string, delete?: boolean }
|
||||||
|
trigger: Partial<Trigger> & { delete?: boolean }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UIStatePatch2 extends UIState {
|
export interface UIStatePatch2 {
|
||||||
full?: UIState
|
full?: UIState
|
||||||
}
|
devices: {[id: string]: Device | null}
|
||||||
|
assignments: {[id: string]: Assignment | null}
|
||||||
|
scripts: {[id: string]: ScriptLine[] | null}
|
||||||
|
triggers: {[id: string]: Trigger | null}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
import { getStateContext } from "$lib/contexts/StateContext.svelte";
|
||||||
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
import DeviceModal from "$lib/modals/DeviceModal.svelte";
|
||||||
import ScriptModal from "$lib/modals/ScriptModal.svelte";
|
import ScriptModal from "$lib/modals/ScriptModal.svelte";
|
||||||
|
import TriggerModal from "$lib/modals/TriggerModal.svelte";
|
||||||
|
|
||||||
const {selectedList} = getSelectedContext();
|
const {selectedList} = getSelectedContext();
|
||||||
const {roomList} = getStateContext();
|
const {roomList} = getStateContext();
|
||||||
@@ -25,6 +26,10 @@
|
|||||||
modal.set({kind: "script.edit"});
|
modal.set({kind: "script.edit"});
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
case 't':
|
||||||
|
modal.set({kind: "trigger.edit"});
|
||||||
|
e.preventDefault();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
|
|
||||||
<DeviceModal />
|
<DeviceModal />
|
||||||
<ScriptModal />
|
<ScriptModal />
|
||||||
|
<TriggerModal />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div.page {
|
div.page {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
Devices map[string]*uistate.Device `json:"devices"`
|
Devices map[string]*uistate.Device `json:"devices"`
|
||||||
Assignments map[uuid.UUID]*uistate.Assignment `json:"assignments"`
|
Assignments map[uuid.UUID]*uistate.Assignment `json:"assignments"`
|
||||||
Scripts map[string][]script.Line `json:"scripts"`
|
Scripts map[string][]script.Line `json:"scripts"`
|
||||||
|
Triggers map[uuid.UUID]*script.Trigger `json:"triggers"`
|
||||||
Full *uistate.Data `json:"full,omitempty"`
|
Full *uistate.Data `json:"full,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +132,10 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
defer ws.Close()
|
defer ws.Close()
|
||||||
|
|
||||||
svc.mu.Lock()
|
svc.mu.Lock()
|
||||||
err = ws.WriteJSON(ChangedPatch{Full: &svc.data})
|
patch := ChangedPatch{Full: gentools.Ptr(svc.data.Copy())}
|
||||||
svc.mu.Unlock()
|
svc.mu.Unlock()
|
||||||
|
|
||||||
|
err = ws.WriteJSON(patch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -157,34 +160,44 @@ func New(addr string) (lucifer3.Service, error) {
|
|||||||
Devices: make(map[string]*uistate.Device),
|
Devices: make(map[string]*uistate.Device),
|
||||||
Assignments: make(map[uuid.UUID]*uistate.Assignment),
|
Assignments: make(map[uuid.UUID]*uistate.Assignment),
|
||||||
Scripts: make(map[string][]script.Line),
|
Scripts: make(map[string][]script.Line),
|
||||||
|
Triggers: make(map[uuid.UUID]*script.Trigger),
|
||||||
Full: nil,
|
Full: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.mu.Lock()
|
svc.mu.Lock()
|
||||||
|
data := svc.data.Copy()
|
||||||
|
svc.mu.Unlock()
|
||||||
|
|
||||||
for _, patch := range patches {
|
for _, patch := range patches {
|
||||||
if patch.Device != nil {
|
if patch.Device != nil {
|
||||||
if patch.Device.Delete {
|
if patch.Device.Delete {
|
||||||
statePatch.Devices[patch.Device.ID] = nil
|
statePatch.Devices[patch.Device.ID] = nil
|
||||||
} else {
|
} else {
|
||||||
statePatch.Devices[patch.Device.ID] = gentools.Ptr(svc.data.Devices[patch.Device.ID])
|
statePatch.Devices[patch.Device.ID] = gentools.Ptr(data.Devices[patch.Device.ID])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if patch.Assignment != nil {
|
if patch.Assignment != nil {
|
||||||
if patch.Assignment.Delete {
|
if patch.Assignment.Delete {
|
||||||
statePatch.Assignments[patch.Assignment.ID] = nil
|
statePatch.Assignments[patch.Assignment.ID] = nil
|
||||||
} else {
|
} else {
|
||||||
statePatch.Assignments[patch.Assignment.ID] = gentools.Ptr(svc.data.Assignments[patch.Assignment.ID])
|
statePatch.Assignments[patch.Assignment.ID] = gentools.Ptr(data.Assignments[patch.Assignment.ID])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if patch.Script != nil {
|
if patch.Script != nil {
|
||||||
if len(patch.Script.Lines) > 0 {
|
if len(patch.Script.Lines) > 0 {
|
||||||
statePatch.Scripts[patch.Script.Name] = svc.data.Scripts[patch.Script.Name]
|
statePatch.Scripts[patch.Script.Name] = data.Scripts[patch.Script.Name]
|
||||||
} else {
|
} else {
|
||||||
statePatch.Scripts[patch.Script.Name] = nil
|
statePatch.Scripts[patch.Script.Name] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if patch.Trigger != nil {
|
||||||
|
if patch.Trigger.Delete {
|
||||||
|
statePatch.Triggers[patch.Trigger.ID] = nil
|
||||||
|
} else {
|
||||||
|
statePatch.Triggers[patch.Trigger.ID] = gentools.Ptr(data.Triggers[patch.Trigger.ID])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
svc.mu.Unlock()
|
|
||||||
|
|
||||||
err := ws.WriteJSON(statePatch)
|
err := ws.WriteJSON(statePatch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -302,12 +315,15 @@ func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
|
|||||||
|
|
||||||
func (s *service) addSub(ch chan uistate.Patch) {
|
func (s *service) addSub(ch chan uistate.Patch) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
s.subs = append(s.subs, ch)
|
s.subs = append(s.subs, ch)
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) removeSub(ch chan uistate.Patch) {
|
func (s *service) removeSub(ch chan uistate.Patch) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
s.subs = append([]chan uistate.Patch{}, s.subs...)
|
s.subs = append([]chan uistate.Patch{}, s.subs...)
|
||||||
for i, sub := range s.subs {
|
for i, sub := range s.subs {
|
||||||
if sub == ch {
|
if sub == ch {
|
||||||
@@ -315,7 +331,6 @@ func (s *service) removeSub(ch chan uistate.Patch) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type commandInput struct {
|
type commandInput struct {
|
||||||
|
|||||||
@@ -113,26 +113,35 @@ func (s *service) runScript(bus *lucifer3.EventBus, lines []Line, match string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else if line.Set != nil {
|
} else if line.Set != nil {
|
||||||
|
var sv *SetVariable
|
||||||
|
|
||||||
switch line.Set.Scope {
|
switch line.Set.Scope {
|
||||||
case "devices":
|
case "devices":
|
||||||
bus.RunCommand(SetVariable{
|
sv = &SetVariable{
|
||||||
Devices: gentools.Map(devices, func(d device.Pointer) string {
|
Devices: gentools.Map(devices, func(d device.Pointer) string {
|
||||||
return d.ID
|
return d.ID
|
||||||
}),
|
}),
|
||||||
Key: line.Set.Key,
|
Key: line.Set.Key,
|
||||||
Value: line.Set.Value,
|
Value: line.Set.Value,
|
||||||
})
|
}
|
||||||
|
bus.RunCommand(sv)
|
||||||
case "match":
|
case "match":
|
||||||
bus.RunCommand(SetVariable{
|
sv = &SetVariable{
|
||||||
Match: gentools.Ptr(match),
|
Match: gentools.Ptr(match),
|
||||||
Key: line.Set.Key,
|
Key: line.Set.Key,
|
||||||
Value: line.Set.Value,
|
Value: line.Set.Value,
|
||||||
})
|
}
|
||||||
case "global":
|
case "global":
|
||||||
bus.RunCommand(SetVariable{
|
sv = &SetVariable{
|
||||||
|
Match: gentools.Ptr(match),
|
||||||
Key: line.Set.Key,
|
Key: line.Set.Key,
|
||||||
Value: line.Set.Value,
|
Value: line.Set.Value,
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv != nil {
|
||||||
|
variables = variables.With(*sv)
|
||||||
|
bus.RunCommand(*sv)
|
||||||
}
|
}
|
||||||
} else if line.Select != nil {
|
} else if line.Select != nil {
|
||||||
matcher := s.resolver.CompileMatcher(line.Select.Match)
|
matcher := s.resolver.CompileMatcher(line.Select.Match)
|
||||||
|
|||||||
@@ -21,6 +21,27 @@ func (v VariableSet) Devices(list []string, key string) string {
|
|||||||
return v["devices:"+strings.Join(list, ";")][key]
|
return v["devices:"+strings.Join(list, ";")][key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v VariableSet) With(sv SetVariable) VariableSet {
|
||||||
|
curr := gentools.CopyMap(v)
|
||||||
|
|
||||||
|
key := "global"
|
||||||
|
if sv.Match != nil {
|
||||||
|
key = "match:" + *sv.Match
|
||||||
|
} else if sv.Devices != nil {
|
||||||
|
key = "devices:" + strings.Join(sv.Devices, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
curr[key] = gentools.CopyMap(curr["global"])
|
||||||
|
|
||||||
|
if sv.Value == "" {
|
||||||
|
delete(curr[key], sv.Key)
|
||||||
|
} else {
|
||||||
|
curr[key][sv.Key] = sv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return curr
|
||||||
|
}
|
||||||
|
|
||||||
type Variables struct {
|
type Variables struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
vars VariableSet
|
vars VariableSet
|
||||||
@@ -47,27 +68,10 @@ func (s *Variables) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {}
|
|||||||
|
|
||||||
func (s *Variables) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
func (s *Variables) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
|
||||||
if sv, ok := command.(SetVariable); ok {
|
if sv, ok := command.(SetVariable); ok {
|
||||||
s.mu.Lock()
|
newVariables := s.Get().With(sv)
|
||||||
curr := gentools.CopyMap(s.vars)
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
key := "global"
|
|
||||||
if sv.Match != nil {
|
|
||||||
key = "match:" + *sv.Match
|
|
||||||
} else if sv.Devices != nil {
|
|
||||||
key = "devices:" + strings.Join(sv.Devices, ";")
|
|
||||||
}
|
|
||||||
|
|
||||||
curr[key] = gentools.CopyMap(curr["global"])
|
|
||||||
|
|
||||||
if sv.Value == "" {
|
|
||||||
delete(curr[key], sv.Key)
|
|
||||||
} else {
|
|
||||||
curr[key][sv.Key] = sv.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.vars = curr
|
s.vars = newVariables
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
Devices map[string]Device `json:"devices"`
|
Devices map[string]Device `json:"devices"`
|
||||||
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
Assignments map[uuid.UUID]Assignment `json:"assignments"`
|
||||||
Scripts map[string][]script.Line `json:"scripts"`
|
Scripts map[string][]script.Line `json:"scripts"`
|
||||||
|
Triggers map[uuid.UUID]script.Trigger `json:"triggers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Data) WithPatch(patches ...Patch) Data {
|
func (d *Data) WithPatch(patches ...Patch) Data {
|
||||||
@@ -121,6 +122,14 @@ func (d *Data) WithPatch(patches ...Patch) Data {
|
|||||||
delete(newData.Scripts, patch.Script.Name)
|
delete(newData.Scripts, patch.Script.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if patch.Trigger != nil {
|
||||||
|
if !patch.Trigger.Delete {
|
||||||
|
newData.Triggers[patch.Trigger.ID] = patch.Trigger.Trigger
|
||||||
|
} else {
|
||||||
|
delete(newData.Triggers, patch.Trigger.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newData
|
return newData
|
||||||
@@ -131,6 +140,7 @@ func (d *Data) Copy() Data {
|
|||||||
Devices: gentools.CopyMap(d.Devices),
|
Devices: gentools.CopyMap(d.Devices),
|
||||||
Assignments: gentools.CopyMap(d.Assignments),
|
Assignments: gentools.CopyMap(d.Assignments),
|
||||||
Scripts: gentools.CopyMap(d.Scripts),
|
Scripts: gentools.CopyMap(d.Scripts),
|
||||||
|
Triggers: gentools.CopyMap(d.Triggers),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type Patch struct {
|
|||||||
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
Assignment *AssignmentPatch `json:"assignment,omitempty"`
|
||||||
Device *DevicePatch `json:"device,omitempty"`
|
Device *DevicePatch `json:"device,omitempty"`
|
||||||
Script *ScriptPatch `json:"script,omitempty"`
|
Script *ScriptPatch `json:"script,omitempty"`
|
||||||
|
Trigger *TriggerPatch `json:"trigger,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Patch) VerboseKey() string {
|
func (e Patch) VerboseKey() string {
|
||||||
@@ -83,3 +84,8 @@ type ScriptPatch struct {
|
|||||||
Name string
|
Name string
|
||||||
Lines []script.Line
|
Lines []script.Line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TriggerPatch struct {
|
||||||
|
script.Trigger
|
||||||
|
Delete bool `json:"delete,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func NewService() lucifer3.ActiveService {
|
|||||||
Devices: map[string]Device{},
|
Devices: map[string]Device{},
|
||||||
Assignments: map[uuid.UUID]Assignment{},
|
Assignments: map[uuid.UUID]Assignment{},
|
||||||
Scripts: map[string][]script.Line{},
|
Scripts: map[string][]script.Line{},
|
||||||
|
Triggers: map[uuid.UUID]script.Trigger{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,17 @@ func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command
|
|||||||
patches = append(patches, Patch{
|
patches = append(patches, Patch{
|
||||||
Script: &ScriptPatch{Name: command.Name, Lines: command.Lines},
|
Script: &ScriptPatch{Name: command.Name, Lines: command.Lines},
|
||||||
})
|
})
|
||||||
|
case script.UpdateTrigger:
|
||||||
|
patches = append(patches, Patch{
|
||||||
|
Trigger: &TriggerPatch{Trigger: script.Trigger(command)},
|
||||||
|
})
|
||||||
|
case script.DeleteTrigger:
|
||||||
|
patches = append(patches, Patch{
|
||||||
|
Trigger: &TriggerPatch{
|
||||||
|
Trigger: script.Trigger{ID: command.ID},
|
||||||
|
Delete: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(patches) > 0 {
|
if len(patches) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user