From 6521d20719d262ad23b5f9d8cbf5bfe102fa271f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 12 May 2024 01:52:34 +0200 Subject: [PATCH] Automatic reboot on a schedule (#86) --- backend/app/api/frames.py | 4 +- backend/app/models/frame.py | 4 + backend/app/tasks/deploy_frame.py | 10 ++ .../8cad2df43b45_reboot_and_control_code.py | 34 ++++++ frontend/package-lock.json | 14 +-- frontend/package.json | 2 +- frontend/src/scenes/frame/frameLogic.ts | 2 + .../panels/FrameDetails/FrameDetails.tsx | 25 +++++ .../panels/FrameSettings/FrameSettings.tsx | 101 ++++++++++++++++-- frontend/src/types.tsx | 11 ++ 10 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 backend/migrations/versions/8cad2df43b45_reboot_and_control_code.py diff --git a/backend/app/api/frames.py b/backend/app/api/frames.py index 263019c5..9b947e56 100644 --- a/backend/app/api/frames.py +++ b/backend/app/api/frames.py @@ -173,7 +173,7 @@ def api_frame_update(id: int): frame = Frame.query.get_or_404(id) fields = ['scenes', 'name', 'frame_host', 'frame_port', 'frame_access_key', 'frame_access', 'ssh_user', 'ssh_pass', 'ssh_port', 'server_host', 'server_port', 'server_api_key', 'width', 'height', 'rotate', 'color', 'interval', 'metrics_interval', 'log_to_file', - 'scaling_mode', 'device', 'debug'] + 'scaling_mode', 'device', 'debug', 'reboot', 'control_code'] defaults = {'frame_port': 8787, 'ssh_port': 22} try: payload = request.json @@ -188,7 +188,7 @@ def api_frame_update(id: int): value = float(value) elif field in ['debug']: value = value == 'true' or value is True - elif field in ['scenes']: + elif field in ['scenes', 'reboot', 'control_code']: if isinstance(value, str): value = json.loads(value) if value is not None else None setattr(frame, field, value) diff --git a/backend/app/models/frame.py b/backend/app/models/frame.py index e7fe6178..373a7bf5 100644 --- a/backend/app/models/frame.py +++ b/backend/app/models/frame.py @@ -40,6 +40,8 @@ class Frame(db.Model): log_to_file = db.Column(db.String(256), nullable=True) debug = db.Column(db.Boolean, nullable=True) last_log_at = db.Column(db.DateTime, nullable=True) + reboot = db.Column(JSON, nullable=True) + control_code = db.Column(JSON, nullable=True) # apps apps = db.Column(JSON, nullable=True) scenes = db.Column(JSON, nullable=True) @@ -77,6 +79,8 @@ def to_dict(self): 'scenes': self.scenes, 'last_log_at': self.last_log_at.replace(tzinfo=timezone.utc).isoformat() if self.last_log_at else None, 'log_to_file': self.log_to_file, + 'reboot': self.reboot, + 'control_code': self.control_code, } def new_frame(name: str, frame_host: str, server_host: str, device: Optional[str] = None, interval: Optional[float] = None) -> Frame: diff --git a/backend/app/tasks/deploy_frame.py b/backend/app/tasks/deploy_frame.py index 6bf75c42..57e97ef1 100644 --- a/backend/app/tasks/deploy_frame.py +++ b/backend/app/tasks/deploy_frame.py @@ -171,6 +171,16 @@ def install_if_necessary(package: str, raise_on_error = True) -> int: # # disable swap while we're at it # exec_command(frame, ssh, "sudo systemctl disable dphys-swapfile.service") + if frame.reboot and frame.reboot.get('enabled') == 'true': + cron_schedule = frame.reboot.get('crontab', '0 0 * * *') + if frame.reboot.get('type') == 'raspberry': + crontab = f"{cron_schedule} root /sbin/shutdown -r now" + else: + crontab = f"{cron_schedule} root systemctl restart frameos.service" + exec_command(frame, ssh, f"echo '{crontab}' | sudo tee /etc/cron.d/frameos-reboot") + else: + exec_command(frame, ssh, "sudo rm -f /etc/cron.d/frameos-reboot") + # restart exec_command(frame, ssh, "sudo systemctl daemon-reload") exec_command(frame, ssh, "sudo systemctl enable frameos.service") diff --git a/backend/migrations/versions/8cad2df43b45_reboot_and_control_code.py b/backend/migrations/versions/8cad2df43b45_reboot_and_control_code.py new file mode 100644 index 00000000..afb5efba --- /dev/null +++ b/backend/migrations/versions/8cad2df43b45_reboot_and_control_code.py @@ -0,0 +1,34 @@ +"""reboot and control code + +Revision ID: 8cad2df43b45 +Revises: 1e2acb9652e8 +Create Date: 2024-05-11 23:16:10.087724 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import sqlite + +# revision identifiers, used by Alembic. +revision = '8cad2df43b45' +down_revision = '1e2acb9652e8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('frame', schema=None) as batch_op: + batch_op.add_column(sa.Column('reboot', sqlite.JSON(), nullable=True)) + batch_op.add_column(sa.Column('control_code', sqlite.JSON(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('frame', schema=None) as batch_op: + batch_op.drop_column('control_code') + batch_op.drop_column('reboot') + + # ### end Alembic commands ### diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ebe8eb56..556c643a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,7 +27,7 @@ "copy-to-clipboard": "^3.3.3", "fast-deep-equal": "^3.1.3", "kea": "^3.1.6", - "kea-forms": "^3.1.1", + "kea-forms": "^3.2.0", "kea-loaders": "^3.0.1", "kea-router": "^3.1.4", "kea-subscriptions": "^3.0.0", @@ -4919,9 +4919,9 @@ } }, "node_modules/kea-forms": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/kea-forms/-/kea-forms-3.1.1.tgz", - "integrity": "sha512-JTxElXdd/YifBe80V6HFEdZ2ISpOBUP9EbwpzKcLiBOZn5PO2+M+jPcTLdR0HSmVWY7fDvLTCu6N16W/nWvG2Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kea-forms/-/kea-forms-3.2.0.tgz", + "integrity": "sha512-R/ECGx6FxOZ2TEJv2GcFNLcQJePUK5Qd4Km81rN/7lBHd2hG4CAJglODVpcolWZ0RtcLcvMhHeTg/iqNz5pynw==", "peerDependencies": { "kea": ">= 3.0.1" } @@ -11254,9 +11254,9 @@ } }, "kea-forms": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/kea-forms/-/kea-forms-3.1.1.tgz", - "integrity": "sha512-JTxElXdd/YifBe80V6HFEdZ2ISpOBUP9EbwpzKcLiBOZn5PO2+M+jPcTLdR0HSmVWY7fDvLTCu6N16W/nWvG2Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/kea-forms/-/kea-forms-3.2.0.tgz", + "integrity": "sha512-R/ECGx6FxOZ2TEJv2GcFNLcQJePUK5Qd4Km81rN/7lBHd2hG4CAJglODVpcolWZ0RtcLcvMhHeTg/iqNz5pynw==", "requires": {} }, "kea-loaders": { diff --git a/frontend/package.json b/frontend/package.json index 609f9378..b8a20775 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,7 +34,7 @@ "copy-to-clipboard": "^3.3.3", "fast-deep-equal": "^3.1.3", "kea": "^3.1.6", - "kea-forms": "^3.1.1", + "kea-forms": "^3.2.0", "kea-loaders": "^3.0.1", "kea-router": "^3.1.4", "kea-subscriptions": "^3.0.0", diff --git a/frontend/src/scenes/frame/frameLogic.ts b/frontend/src/scenes/frame/frameLogic.ts index 3ce0cafe..3c52f9bf 100644 --- a/frontend/src/scenes/frame/frameLogic.ts +++ b/frontend/src/scenes/frame/frameLogic.ts @@ -35,6 +35,8 @@ const FRAME_KEYS: (keyof FrameType)[] = [ 'scenes', 'debug', 'log_to_file', + 'reboot', + 'control_code', ] function cleanBackgroundColor(color: string): string { diff --git a/frontend/src/scenes/frame/panels/FrameDetails/FrameDetails.tsx b/frontend/src/scenes/frame/panels/FrameDetails/FrameDetails.tsx index f2d459a7..8c9710b7 100644 --- a/frontend/src/scenes/frame/panels/FrameDetails/FrameDetails.tsx +++ b/frontend/src/scenes/frame/panels/FrameDetails/FrameDetails.tsx @@ -146,6 +146,31 @@ export function FrameDetails({ className }: DetailsProps) { Log to file: {frame.log_to_file || disabled} + + Reboot: + + {frame.reboot?.enabled === 'true' ? ( + <> + {String(frame.reboot?.type)} at {String(frame.reboot?.crontab)} + + ) : ( + 'disabled' + )} + + + {/* + QR control code: + + {frame.control_code?.enabled === 'true' ? ( + <> + {String(frame.control_code?.position)}, size: {String(frame.control_code?.size)}, padding:{' '} + {String(frame.control_code?.padding)} + + ) : ( + 'disabled' + )} + + */} Debug logging: {frame.debug ? 'enabled' : 'disabled'} diff --git a/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx b/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx index 34598de1..6dddad67 100644 --- a/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx +++ b/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx @@ -2,7 +2,7 @@ import { useActions, useValues } from 'kea' import clsx from 'clsx' import { Button } from '../../../../components/Button' import { framesModel } from '../../../../models/framesModel' -import { Form } from 'kea-forms' +import { Form, Group } from 'kea-forms' import { TextInput } from '../../../../components/TextInput' import { Select } from '../../../../components/Select' import { frameLogic } from '../../frameLogic' @@ -16,7 +16,7 @@ export interface FrameSettingsProps { } export function FrameSettings({ className }: FrameSettingsProps) { - const { frameId, frame, frameFormTouches } = useValues(frameLogic) + const { frameId, frame, frameForm, frameFormTouches } = useValues(frameLogic) const { touchFrameFormField, setFrameFormValues } = useActions(frameLogic) const { deleteFrame } = useActions(framesModel) @@ -281,16 +281,95 @@ export function FrameSettings({ className }: FrameSettingsProps) { placeholder="e.g. /srv/frameos/logs/frame-{date}.log" required /> - {' '} - - + + {String(frameForm.reboot?.enabled) === 'true' && ( +
+ + + +
+ )} + + {/* + + + + + + + + + + + )} + */} )} diff --git a/frontend/src/types.tsx b/frontend/src/types.tsx index ef65c1ea..854cb67d 100644 --- a/frontend/src/types.tsx +++ b/frontend/src/types.tsx @@ -28,6 +28,17 @@ export interface FrameType { debug?: boolean last_log_at?: string log_to_file?: string + reboot?: { + enabled?: 'true' | 'false' + crontab?: string + type?: 'frameos' | 'raspberry' + } + control_code?: { + enabled?: 'true' | 'false' + position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' + size?: string + padding?: string + } } export interface TemplateType {