Browse Source

trigger editor

beelzebub
Gisle Aune 1 year ago
parent
commit
16e5726cbc
  1. 2
      frontend/src/lib/components/HSplit.svelte
  2. 4
      frontend/src/lib/components/Modal.svelte
  3. 1
      frontend/src/lib/contexts/ModalContext.svelte
  4. 32
      frontend/src/lib/contexts/StateContext.svelte
  5. 181
      frontend/src/lib/modals/TriggerModal.svelte
  6. 10
      frontend/src/lib/models/command.ts
  7. 81
      frontend/src/lib/models/script.ts
  8. 12
      frontend/src/lib/models/uistate.ts
  9. 6
      frontend/src/routes/+page.svelte
  10. 29
      services/httpapiv1/service.go
  11. 21
      services/script/service.go
  12. 42
      services/script/variables.go
  13. 16
      services/uistate/data.go
  14. 6
      services/uistate/patch.go
  15. 12
      services/uistate/service.go

2
frontend/src/lib/components/HSplit.svelte

@ -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>

4
frontend/src/lib/components/Modal.svelte

@ -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;

1
frontend/src/lib/contexts/ModalContext.svelte

@ -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>

32
frontend/src/lib/contexts/StateContext.svelte

@ -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) {
state.devices[deviceId] = patch.devices[deviceId]
if (patch.devices[deviceId] !== null) {
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>

181
frontend/src/lib/modals/TriggerModal.svelte

@ -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>

10
frontend/src/lib/models/command.ts

@ -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
} }

81
frontend/src/lib/models/script.ts

@ -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),
}
} }

12
frontend/src/lib/models/uistate.ts

@ -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}
}

6
frontend/src/routes/+page.svelte

@ -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 {

29
services/httpapiv1/service.go

@ -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 {

21
services/script/service.go

@ -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)

42
services/script/variables.go

@ -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()
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
}
newVariables := s.Get().With(sv)
s.mu.Lock() s.mu.Lock()
s.vars = curr
s.vars = newVariables
s.mu.Unlock() s.mu.Unlock()
} }
} }

16
services/uistate/data.go

@ -12,9 +12,10 @@ import (
) )
type Data struct { type Data struct {
Devices map[string]Device `json:"devices"`
Assignments map[uuid.UUID]Assignment `json:"assignments"`
Scripts map[string][]script.Line `json:"scripts"`
Devices map[string]Device `json:"devices"`
Assignments map[uuid.UUID]Assignment `json:"assignments"`
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),
} }
} }

6
services/uistate/patch.go

@ -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"`
}

12
services/uistate/service.go

@ -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 {

Loading…
Cancel
Save