Skip to content

Commit

Permalink
v2.4 Feature - Time Clock Triggers - Scheduled Presets (#163)
Browse files Browse the repository at this point in the history
* bump

* TS Docs aren't actually provided as far as I can see

* Bump

* Add database structure

* Add edit page

* Add config option to macro preset

* Allow presets to be recalled over http

* Bump version

* Improve locked message, and fix logic error in web server

* Basic work on time clock triggers

* Remove time clock triggers

* Lint

* Remove stats file

* Lint

* Update folder.ts

* Add badge showing preset type

* Add parent name to folders

* Add duplicate preset button

* Refactor folder icons to allow them to be used with other buttons

* Add preset icons, and make icons searchable

* Add more icons

* Improve admin pin entry

* Update settings.json

* Start UI

* Bump

* Fix imports

* Add more ui

* Remove webpack cache to fix database error in electron/forge#2412 (comment)

* Radically simplify icon file

* Basic UI

* UI tweaks

* Edit functionality complete

* Build out function

* Update NavbarItem.tsx

* Fix bugs

* Time clock trigger bugfixes

* Clarify skip behaviour

* Lint

* Closes #164 by removing effect mode

* Update index.ts

* Add error handling to sampling mode

* Add docs
  • Loading branch information
Jbithell authored Jul 14, 2023
1 parent e68131b commit 3750d5d
Show file tree
Hide file tree
Showing 42 changed files with 1,024 additions and 233 deletions.
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Please describe the changes
- [ ] Preset/Faders/Folders sort order is maintained
- [ ] Presets can be triggered by HTTP requests

### Preset Triggers

- [ ] Presets can be triggered by HTTP requests
- [ ] Time clock triggers recall presets successfully
- [ ] Time clock triggers do not recall when device locked

### Preset Types

#### sACN
Expand Down
4 changes: 4 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"DEV_MODE": "true"
},
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**/*.js",
"${workspaceFolder}/node_modules/**/*.js",
]
}
]
}
23 changes: 23 additions & 0 deletions docs/docs/user-guide/admin/scheduled-presets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
sidebar_position: 30
title: Scheduled Presets
---

The Scheduled Presets page allows you to define times of the day when presets are triggered automatically.

To start, use the plus button to add a new schedule. Next, select the days of the week you'd like the schedule to run on, and the time to run during those days. Finally, select the preset you'd like to run at that time, and click save.

To trigger a preset at multiple times during the day, simply add multiple schedules for that preset.

![scheduled presets page](@site/static/img/tutorial/admin/admin-scheduled-presets.png)

The cog icon on the right side of each preset allows you to edit the advanced settings for that preset.

![scheduled presets cog page](@site/static/img/tutorial/admin/admin-scheduled-presets-cog.png)

The advanced settings allow you to set the following:

- Notes: A description of why the schedule exists as a reminder in the administrator menu - this can help you differentiate between large numbers of schedules.
- Enabled: whether the schedule is enabled or not. If disabled, the schedule will not run.
- Run when locked: if the control panel is locked, the schedule will still run if this is enabled.
- Timeout: how many minutes to keep trying to trigger the preset for. If the preset is not triggered within this time, the schedule will be skipped for that day. This is particularly relevant if ParadisePi is powered off at the time the schedule is due to run, and later powered on - if the timeout plus the scheduled time is earlier than the time ParadisePi boots, then it will be skipped.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "paradisepi",
"version": "2.3.0",
"version": "2.4.0",
"description": "Raspberry Pi based sACN lighting/OSC sound facility control panel",
"main": ".webpack/main",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/api/config/configRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const configRouter = (
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
payload: apiObject
): Promise<apiObject> => {
logger.debug('Preset router has a request', { path, method, payload })
logger.debug('Config router has a request', { path, method, payload })
return new Promise((resolve, reject) => {
if (method === 'POST') {
let restartE131 = false
Expand Down
15 changes: 8 additions & 7 deletions src/api/database.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DatabasePreset, PresetRepository } from './../database/repository/preset'
import { DatabaseFolder, FolderRepository } from './../database/repository/folder'
import ip from 'ip'
import { version } from './../../package.json'
import { ConfigRepository } from './../database/repository/config'
import { DatabaseFader, FaderRepository } from './../database/repository/fader'
import { DatabaseFolder, FolderRepository } from './../database/repository/folder'
import { DatabasePreset, PresetRepository } from './../database/repository/preset'
import { DatabaseTimeClockTrigger, TimeClockTriggersRepository } from './../database/repository/timeClockTrigger'
import { getOperatingSystemName } from './about/operatingSystem/info'
import { version } from './../../package.json'
import { broadcast } from './broadcast'
import ip from 'ip'
import { DatabaseFader, FaderRepository } from './../database/repository/fader'

export interface Database {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -42,10 +43,10 @@ export interface Database {
e131Frequency: number
e131FadeTime: number
e131Sampler_time: number
e131Sampler_effectMode: number
}
}
presets: Array<DatabasePreset>
timeClockTriggers: Array<DatabaseTimeClockTrigger>
folders: {
[key: number]: DatabaseFolder
}
Expand Down Expand Up @@ -91,10 +92,10 @@ export const createDatabaseObject = async (message: string): Promise<Database> =
e131Frequency: parseInt(await ConfigRepository.getItem('e131Frequency')),
e131FadeTime: parseInt(await ConfigRepository.getItem('e131FadeTime')),
e131Sampler_time: parseInt(await ConfigRepository.getItem('e131Sampler_time')),
e131Sampler_effectMode: parseInt(await ConfigRepository.getItem('e131Sampler_effectMode')),
},
},
presets: await PresetRepository.getAll(),
timeClockTriggers: await TimeClockTriggersRepository.getAll(),
folders: await FolderRepository.getAll(),
faders: await FaderRepository.getAll(),
}
Expand Down
6 changes: 3 additions & 3 deletions src/api/preset/presetRouter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Preset } from './../../database/model/Preset'
import { DatabasePreset, PresetRepository } from './../../database/repository/preset'
import { createDatabaseObject, Database, sendDatabaseObject } from './../database'
import axios from 'axios'
import { parseJSON } from '../parseUserJson'
import { Preset } from './../../database/model/Preset'
import { ConfigRepository } from './../../database/repository/config'
import { DatabasePreset, PresetRepository } from './../../database/repository/preset'
import { Database, createDatabaseObject, sendDatabaseObject } from './../database'
/**
* This is a REST router for the preset API.
* @param path - The path requested by the original route requestor
Expand Down
15 changes: 10 additions & 5 deletions src/api/router.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { reboot } from './../electron/windowUtilities'
import { aboutRouter } from './about/aboutRouter'
import { createDatabaseObject } from './database'
import { presetRouter } from './preset/presetRouter'
import { folderRouter } from './folder/folderRouter'
import { configRouter } from './config/configRouter'
import { outputModulesRouter } from './outputModules/outputModulesRouter'
import { reboot } from './../electron/windowUtilities'
import { createDatabaseObject } from './database'
import { faderRouter } from './fader/faderRouter'
import { folderRouter } from './folder/folderRouter'
import { createImagesObject } from './images'
import { outputModulesRouter } from './outputModules/outputModulesRouter'
import { presetRouter } from './preset/presetRouter'
import { timeClockTriggersRouter } from './timeClockTriggers/timeClockTriggers'

/**
* This is a REST router that triages all requests and sends them to relevant routers
Expand Down Expand Up @@ -52,6 +53,10 @@ export const routeRequest = (
// {@link configRouter} - this router handles all about requests for the /config path
resolve(configRouter(pathArr.slice(1), method, payload))
break
case 'timeClockTriggers':
// {@link timeClockTriggers} - the timeClockTriggers router handles all about requests for the /timeClockTriggers path
resolve(timeClockTriggersRouter(pathArr.slice(1), method, payload))
break
case 'outputModules':
// {@link outputModulesRouter} - this router handles all about requests for the /outputModules path
resolve(outputModulesRouter(pathArr.slice(1), method, payload))
Expand Down
29 changes: 29 additions & 0 deletions src/api/timeClockTriggers/timeClockTriggers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Database, createDatabaseObject, sendDatabaseObject } from '../database'
import { DatabaseTimeClockTrigger, TimeClockTriggersRepository } from './../../database/repository/timeClockTrigger'
/**
* This is a REST router for the preset API.
* @param path - The path requested by the original route requestor
* @param method - The method requested by the original route requestor
* @param payload - Any payload sent
* @returns the retrieved response from the given route
* @throws an error if the requested route is not found
*/
export const timeClockTriggersRouter = (
path: Array<string>,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
payload: apiObject
): Promise<apiObject> => {
logger.debug('Time clock trigger router has a request', { path, method, payload })
return new Promise((resolve, reject) => {
if (method === 'PUT') {
return TimeClockTriggersRepository.setAllFromApp(payload as Array<DatabaseTimeClockTrigger>)
.then(() => {
return createDatabaseObject('updating all time clock triggers in bulk')
})
.then((response: Database) => {
sendDatabaseObject(response)
resolve({})
})
} else reject(new Error('Path not found'))
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useEffect } from 'react'
import { Group, ActionIcon, Button, Text, Checkbox, NumberInput } from '@mantine/core'
import { useForm } from '@mantine/form'
import { FaTrash } from '@react-icons/all-files/fa/FaTrash'
import { randomId } from '@mantine/hooks'
import { InputProps } from '../../../../InputProps'

interface Trigger {
time: string
enabled: boolean
timeout: number
countdownWarning: number
key: string
}
interface FormValues {
triggers: Array<Trigger>
}

export const TimeClockTriggersEditor = (props: InputProps) => {
const form = useForm<FormValues>({
initialValues: {
triggers: [],
},
})
useEffect(() => {
if (props.value !== null) {
const valueObject = JSON.parse(props.value) || {}
form.setValues({
triggers: valueObject.map((item: Trigger) => ({
time: item.time,
key: item.key,
})),
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.value])
return (
<>
<Group mt="xs">
<Text mt={'md'}>Time Clock Triggers</Text>
<Button
mt={'md'}
size="sm"
onClick={() =>
form.insertListItem('triggers', {
time: '',
enabled: true,
timeout: 60,
countdownWarning: 0,
key: randomId(),
})
}
>
Add Trigger
</Button>
{form.isDirty() ? (
<Button mt={'md'} size="sm" onClick={() => props.onChange(JSON.stringify(form.values.triggers))}>
Apply
</Button>
) : null}
</Group>
<Text size={'sm'}>Time Clock Triggers will recall this preset at the following times (if enabled)</Text>
{form.values.triggers.map((item, index) => (
<Group key={item.key} mt="xs">
<NumberInput
label="Trigger Time"
max={2359}
min={0}
parser={value =>
value.match(/\d(?=(?:\D*\d){0,3}$)/g) ? value.match(/\d(?=(?:\D*\d){0,3}$)/g).join('') : ''
}
formatter={value =>
!Number.isNaN(parseFloat(value))
? value.replace(/\b\d{1,4}\b/g, match => {
const paddedNumber = match.padStart(4, '0')
const hours = paddedNumber.slice(0, 2)
const minutes = paddedNumber.slice(2)
return `${hours}:${minutes}`
})
: ''
}
{...form.getInputProps(`triggers.${index}.time`)}
/>
<Checkbox
mt={'lg'}
size={'lg'}
label="Enabled"
{...form.getInputProps(`triggers.${index}.enabled`, { type: 'checkbox' })}
/>
<ActionIcon
mt={'lg'}
color="red"
variant="transparent"
onClick={() => form.removeListItem('triggers', index)}
>
<FaTrash />
</ActionIcon>
</Group>
))}

<Group position="center" mt="md"></Group>
</>
)
}
Loading

0 comments on commit 3750d5d

Please sign in to comment.