Skip to content

Commit 6521d20

Browse files
authored
Automatic reboot on a schedule (#86)
1 parent 9242e2a commit 6521d20

File tree

10 files changed

+186
-21
lines changed

10 files changed

+186
-21
lines changed

backend/app/api/frames.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def api_frame_update(id: int):
173173
frame = Frame.query.get_or_404(id)
174174
fields = ['scenes', 'name', 'frame_host', 'frame_port', 'frame_access_key', 'frame_access', 'ssh_user', 'ssh_pass', 'ssh_port', 'server_host',
175175
'server_port', 'server_api_key', 'width', 'height', 'rotate', 'color', 'interval', 'metrics_interval', 'log_to_file',
176-
'scaling_mode', 'device', 'debug']
176+
'scaling_mode', 'device', 'debug', 'reboot', 'control_code']
177177
defaults = {'frame_port': 8787, 'ssh_port': 22}
178178
try:
179179
payload = request.json
@@ -188,7 +188,7 @@ def api_frame_update(id: int):
188188
value = float(value)
189189
elif field in ['debug']:
190190
value = value == 'true' or value is True
191-
elif field in ['scenes']:
191+
elif field in ['scenes', 'reboot', 'control_code']:
192192
if isinstance(value, str):
193193
value = json.loads(value) if value is not None else None
194194
setattr(frame, field, value)

backend/app/models/frame.py

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class Frame(db.Model):
4040
log_to_file = db.Column(db.String(256), nullable=True)
4141
debug = db.Column(db.Boolean, nullable=True)
4242
last_log_at = db.Column(db.DateTime, nullable=True)
43+
reboot = db.Column(JSON, nullable=True)
44+
control_code = db.Column(JSON, nullable=True)
4345
# apps
4446
apps = db.Column(JSON, nullable=True)
4547
scenes = db.Column(JSON, nullable=True)
@@ -77,6 +79,8 @@ def to_dict(self):
7779
'scenes': self.scenes,
7880
'last_log_at': self.last_log_at.replace(tzinfo=timezone.utc).isoformat() if self.last_log_at else None,
7981
'log_to_file': self.log_to_file,
82+
'reboot': self.reboot,
83+
'control_code': self.control_code,
8084
}
8185

8286
def new_frame(name: str, frame_host: str, server_host: str, device: Optional[str] = None, interval: Optional[float] = None) -> Frame:

backend/app/tasks/deploy_frame.py

+10
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ def install_if_necessary(package: str, raise_on_error = True) -> int:
171171
# # disable swap while we're at it
172172
# exec_command(frame, ssh, "sudo systemctl disable dphys-swapfile.service")
173173

174+
if frame.reboot and frame.reboot.get('enabled') == 'true':
175+
cron_schedule = frame.reboot.get('crontab', '0 0 * * *')
176+
if frame.reboot.get('type') == 'raspberry':
177+
crontab = f"{cron_schedule} root /sbin/shutdown -r now"
178+
else:
179+
crontab = f"{cron_schedule} root systemctl restart frameos.service"
180+
exec_command(frame, ssh, f"echo '{crontab}' | sudo tee /etc/cron.d/frameos-reboot")
181+
else:
182+
exec_command(frame, ssh, "sudo rm -f /etc/cron.d/frameos-reboot")
183+
174184
# restart
175185
exec_command(frame, ssh, "sudo systemctl daemon-reload")
176186
exec_command(frame, ssh, "sudo systemctl enable frameos.service")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""reboot and control code
2+
3+
Revision ID: 8cad2df43b45
4+
Revises: 1e2acb9652e8
5+
Create Date: 2024-05-11 23:16:10.087724
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import sqlite
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '8cad2df43b45'
14+
down_revision = '1e2acb9652e8'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
with op.batch_alter_table('frame', schema=None) as batch_op:
22+
batch_op.add_column(sa.Column('reboot', sqlite.JSON(), nullable=True))
23+
batch_op.add_column(sa.Column('control_code', sqlite.JSON(), nullable=True))
24+
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade():
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
with op.batch_alter_table('frame', schema=None) as batch_op:
31+
batch_op.drop_column('control_code')
32+
batch_op.drop_column('reboot')
33+
34+
# ### end Alembic commands ###

frontend/package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"copy-to-clipboard": "^3.3.3",
3535
"fast-deep-equal": "^3.1.3",
3636
"kea": "^3.1.6",
37-
"kea-forms": "^3.1.1",
37+
"kea-forms": "^3.2.0",
3838
"kea-loaders": "^3.0.1",
3939
"kea-router": "^3.1.4",
4040
"kea-subscriptions": "^3.0.0",

frontend/src/scenes/frame/frameLogic.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const FRAME_KEYS: (keyof FrameType)[] = [
3535
'scenes',
3636
'debug',
3737
'log_to_file',
38+
'reboot',
39+
'control_code',
3840
]
3941

4042
function cleanBackgroundColor(color: string): string {

frontend/src/scenes/frame/panels/FrameDetails/FrameDetails.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,31 @@ export function FrameDetails({ className }: DetailsProps) {
146146
<td className="text-blue-200 text-right">Log to file:</td>
147147
<td className="break-words">{frame.log_to_file || <em>disabled</em>}</td>
148148
</tr>
149+
<tr>
150+
<td className="text-blue-200 text-right">Reboot:</td>
151+
<td className="break-words">
152+
{frame.reboot?.enabled === 'true' ? (
153+
<>
154+
{String(frame.reboot?.type)} at {String(frame.reboot?.crontab)}
155+
</>
156+
) : (
157+
'disabled'
158+
)}
159+
</td>
160+
</tr>
161+
{/* <tr>
162+
<td className="text-blue-200 text-right">QR control code:</td>
163+
<td className="break-words">
164+
{frame.control_code?.enabled === 'true' ? (
165+
<>
166+
{String(frame.control_code?.position)}, size: {String(frame.control_code?.size)}, padding:{' '}
167+
{String(frame.control_code?.padding)}
168+
</>
169+
) : (
170+
'disabled'
171+
)}
172+
</td>
173+
</tr> */}
149174
<tr>
150175
<td className="text-blue-200 text-right">Debug logging:</td>
151176
<td className="break-words">{frame.debug ? 'enabled' : 'disabled'}</td>

frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx

+90-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useActions, useValues } from 'kea'
22
import clsx from 'clsx'
33
import { Button } from '../../../../components/Button'
44
import { framesModel } from '../../../../models/framesModel'
5-
import { Form } from 'kea-forms'
5+
import { Form, Group } from 'kea-forms'
66
import { TextInput } from '../../../../components/TextInput'
77
import { Select } from '../../../../components/Select'
88
import { frameLogic } from '../../frameLogic'
@@ -16,7 +16,7 @@ export interface FrameSettingsProps {
1616
}
1717

1818
export function FrameSettings({ className }: FrameSettingsProps) {
19-
const { frameId, frame, frameFormTouches } = useValues(frameLogic)
19+
const { frameId, frame, frameForm, frameFormTouches } = useValues(frameLogic)
2020
const { touchFrameFormField, setFrameFormValues } = useActions(frameLogic)
2121
const { deleteFrame } = useActions(framesModel)
2222

@@ -281,16 +281,95 @@ export function FrameSettings({ className }: FrameSettingsProps) {
281281
placeholder="e.g. /srv/frameos/logs/frame-{date}.log"
282282
required
283283
/>
284-
</Field>{' '}
285-
<Field name="debug" label="Debug logging (noisy)">
286-
<Select
287-
name="debug"
288-
options={[
289-
{ value: 'false', label: 'Disabled' },
290-
{ value: 'true', label: 'Enabled' },
291-
]}
292-
/>
293284
</Field>
285+
<Group name="reboot">
286+
<Field name="enabled" label="Automatic reboot">
287+
<Select
288+
name="enabled"
289+
options={[
290+
{ value: 'false', label: 'Disabled' },
291+
{ value: 'true', label: 'Enabled' },
292+
]}
293+
/>
294+
</Field>
295+
{String(frameForm.reboot?.enabled) === 'true' && (
296+
<div className="pl-4 space-y-4">
297+
<Field name="crontab" label="Reboot time">
298+
<Select
299+
name="crontab"
300+
options={[
301+
{ value: '0 0 * * *', label: '00:00' },
302+
{ value: '1 0 * * *', label: '01:00' },
303+
{ value: '2 0 * * *', label: '02:00' },
304+
{ value: '3 0 * * *', label: '03:00' },
305+
{ value: '4 0 * * *', label: '04:00' },
306+
{ value: '5 0 * * *', label: '05:00' },
307+
{ value: '6 0 * * *', label: '06:00' },
308+
{ value: '7 0 * * *', label: '07:00' },
309+
{ value: '8 0 * * *', label: '08:00' },
310+
{ value: '9 0 * * *', label: '09:00' },
311+
{ value: '10 0 * * *', label: '10:00' },
312+
{ value: '11 0 * * *', label: '11:00' },
313+
{ value: '12 0 * * *', label: '12:00' },
314+
{ value: '13 0 * * *', label: '13:00' },
315+
{ value: '14 0 * * *', label: '14:00' },
316+
{ value: '15 0 * * *', label: '15:00' },
317+
{ value: '16 0 * * *', label: '16:00' },
318+
{ value: '17 0 * * *', label: '17:00' },
319+
{ value: '18 0 * * *', label: '18:00' },
320+
{ value: '19 0 * * *', label: '19:00' },
321+
{ value: '20 0 * * *', label: '20:00' },
322+
{ value: '21 0 * * *', label: '21:00' },
323+
{ value: '22 0 * * *', label: '22:00' },
324+
{ value: '23 0 * * *', label: '23:00' },
325+
]}
326+
/>
327+
</Field>
328+
<Field name="type" label="What to reboot">
329+
<Select
330+
name="type"
331+
options={[
332+
{ value: 'frameos', label: 'FrameOS' },
333+
{ value: 'raspberry', label: 'System reboot' },
334+
]}
335+
/>
336+
</Field>
337+
</div>
338+
)}
339+
</Group>
340+
{/* <Group name="control_code">
341+
<Field name="enabled" label="QR Control Code">
342+
<Select
343+
name="enabled"
344+
options={[
345+
{ value: 'false', label: 'Disabled' },
346+
{ value: 'true', label: 'Enabled' },
347+
]}
348+
/>
349+
</Field>
350+
{String(frameForm.control_code?.enabled) === 'true' && (
351+
<div className="pl-4 space-y-4">
352+
<Field name="position" label="Position">
353+
<Select
354+
name="position"
355+
options={[
356+
{ value: 'top-left', label: 'Top Left' },
357+
{ value: 'top-right', label: 'Top Right' },
358+
{ value: 'bottom-left', label: 'Bottom Left' },
359+
{ value: 'bottom-right', label: 'Bottom Right' },
360+
{ value: 'center', label: 'Center' },
361+
]}
362+
/>
363+
</Field>
364+
<Field name="size" label="Size of each square in pixels">
365+
<TextInput name="size" placeholder="2" />
366+
</Field>
367+
<Field name="padding" label="Padding in pixels">
368+
<TextInput name="padding" placeholder="0" />
369+
</Field>
370+
</div>
371+
)}
372+
</Group> */}
294373
</Form>
295374
</>
296375
)}

frontend/src/types.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ export interface FrameType {
2828
debug?: boolean
2929
last_log_at?: string
3030
log_to_file?: string
31+
reboot?: {
32+
enabled?: 'true' | 'false'
33+
crontab?: string
34+
type?: 'frameos' | 'raspberry'
35+
}
36+
control_code?: {
37+
enabled?: 'true' | 'false'
38+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'
39+
size?: string
40+
padding?: string
41+
}
3142
}
3243

3344
export interface TemplateType {

0 commit comments

Comments
 (0)