mirror of
https://github.com/dw-0/kiauh.git
synced 2025-12-24 16:23:36 +05:00
feat: add KlipperScreen
Signed-off-by: Dominik Willner <th33xitus@gmail.com>
This commit is contained in:
16
kiauh/components/klipperscreen/__init__.py
Normal file
16
kiauh/components/klipperscreen/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.backup_manager import BACKUP_ROOT_DIR
|
||||||
|
|
||||||
|
KLIPPERSCREEN_REPO = "https://github.com/KlipperScreen/KlipperScreen.git"
|
||||||
|
KLIPPERSCREEN_DIR = Path.home().joinpath("KlipperScreen")
|
||||||
|
KLIPPERSCREEN_ENV = Path.home().joinpath(".KlipperScreen-env")
|
||||||
|
KLIPPERSCREEN_BACKUP_DIR = BACKUP_ROOT_DIR.joinpath("klipperscreen-backups")
|
||||||
217
kiauh/components/klipperscreen/klipperscreen.py
Normal file
217
kiauh/components/klipperscreen/klipperscreen.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# ======================================================================= #
|
||||||
|
# Copyright (C) 2020 - 2024 Dominik Willner <th33xitus@gmail.com> #
|
||||||
|
# #
|
||||||
|
# This file is part of KIAUH - Klipper Installation And Update Helper #
|
||||||
|
# https://github.com/dw-0/kiauh #
|
||||||
|
# #
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license #
|
||||||
|
# ======================================================================= #
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import run, CalledProcessError
|
||||||
|
from typing import List, Dict, Literal, Union
|
||||||
|
|
||||||
|
from components.klipper.klipper import Klipper
|
||||||
|
from components.klipperscreen import (
|
||||||
|
KLIPPERSCREEN_DIR,
|
||||||
|
KLIPPERSCREEN_REPO,
|
||||||
|
KLIPPERSCREEN_ENV,
|
||||||
|
KLIPPERSCREEN_BACKUP_DIR,
|
||||||
|
)
|
||||||
|
from components.moonraker.moonraker import Moonraker
|
||||||
|
from core.backup_manager.backup_manager import BackupManager
|
||||||
|
from core.instance_manager.instance_manager import InstanceManager
|
||||||
|
from core.settings.kiauh_settings import KiauhSettings
|
||||||
|
from utils.common import get_install_status
|
||||||
|
from utils.config_utils import add_config_section, remove_config_section
|
||||||
|
from utils.constants import SYSTEMD
|
||||||
|
from utils.fs_utils import remove_with_sudo
|
||||||
|
from utils.git_utils import (
|
||||||
|
git_clone_wrapper,
|
||||||
|
git_pull_wrapper,
|
||||||
|
get_repo_name,
|
||||||
|
get_local_commit,
|
||||||
|
get_remote_commit,
|
||||||
|
)
|
||||||
|
from utils.input_utils import get_confirm
|
||||||
|
from utils.logger import Logger
|
||||||
|
from utils.sys_utils import (
|
||||||
|
check_python_version,
|
||||||
|
check_package_install,
|
||||||
|
install_system_packages,
|
||||||
|
control_systemd_service,
|
||||||
|
install_python_requirements,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install_klipperscreen() -> None:
|
||||||
|
Logger.print_status("Installing KlipperScreen ...")
|
||||||
|
|
||||||
|
if not check_python_version(3, 7):
|
||||||
|
return
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances = mr_im.instances
|
||||||
|
if not mr_instances:
|
||||||
|
# TODO: add moonraker not found dialog
|
||||||
|
print("Moonraker not found!")
|
||||||
|
if not get_confirm(
|
||||||
|
"Continue KlipperScreen installation?",
|
||||||
|
allow_go_back=True,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
package_list = ["wget", "curl", "unzip", "dfu-util"]
|
||||||
|
packages = check_package_install(package_list)
|
||||||
|
if packages:
|
||||||
|
install_system_packages(packages)
|
||||||
|
|
||||||
|
git_clone_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR)
|
||||||
|
|
||||||
|
try:
|
||||||
|
script = f"{KLIPPERSCREEN_DIR}/scripts/KlipperScreen-install.sh"
|
||||||
|
run(script, shell=True, check=True)
|
||||||
|
if mr_instances:
|
||||||
|
patch_klipperscreen_update_manager(mr_instances)
|
||||||
|
mr_im.restart_all_instance()
|
||||||
|
else:
|
||||||
|
Logger.print_info(
|
||||||
|
"Moonraker is not installed! Cannot add KlipperScreen to update manager!"
|
||||||
|
)
|
||||||
|
Logger.print_ok("KlipperScreen successfully installed!")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Error installing KlipperScreen:\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def patch_klipperscreen_update_manager(instances: List[Moonraker]) -> None:
|
||||||
|
env_py = f"{KLIPPERSCREEN_ENV}/bin/python"
|
||||||
|
add_config_section(
|
||||||
|
section="update_manager KlipperScreen",
|
||||||
|
instances=instances,
|
||||||
|
options=[
|
||||||
|
("type", "git_repo"),
|
||||||
|
("path", str(KLIPPERSCREEN_DIR)),
|
||||||
|
("orgin", KLIPPERSCREEN_REPO),
|
||||||
|
("env", env_py),
|
||||||
|
("requirements", "scripts/KlipperScreen-requirements.txt"),
|
||||||
|
("install_script", "scripts/KlipperScreen-install.sh"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_klipperscreen() -> None:
|
||||||
|
try:
|
||||||
|
control_systemd_service("KlipperScreen", "stop")
|
||||||
|
|
||||||
|
if not KLIPPERSCREEN_DIR.exists():
|
||||||
|
Logger.print_info(
|
||||||
|
"KlipperScreen does not seem to be installed! Skipping ..."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
Logger.print_status("Updating KlipperScreen ...")
|
||||||
|
|
||||||
|
control_systemd_service("KlipperScreen", "stop")
|
||||||
|
|
||||||
|
settings = KiauhSettings()
|
||||||
|
if settings.get("kiauh", "backup_before_update"):
|
||||||
|
backup_klipperscreen_dir()
|
||||||
|
|
||||||
|
git_pull_wrapper(KLIPPERSCREEN_REPO, KLIPPERSCREEN_DIR)
|
||||||
|
|
||||||
|
requirements = KLIPPERSCREEN_DIR.joinpath(
|
||||||
|
"/scripts/KlipperScreen-requirements.txt"
|
||||||
|
)
|
||||||
|
install_python_requirements(KLIPPERSCREEN_ENV, requirements)
|
||||||
|
|
||||||
|
control_systemd_service("KlipperScreen", "start")
|
||||||
|
|
||||||
|
Logger.print_ok("KlipperScreen updated successfully.", end="\n\n")
|
||||||
|
except CalledProcessError as e:
|
||||||
|
Logger.print_error(f"Something went wrong! Please try again...\n{e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def get_klipperscreen_status() -> (
|
||||||
|
Dict[
|
||||||
|
Literal["status", "status_code", "repo", "local", "remote"],
|
||||||
|
Union[str, int],
|
||||||
|
]
|
||||||
|
):
|
||||||
|
files = [
|
||||||
|
KLIPPERSCREEN_DIR,
|
||||||
|
KLIPPERSCREEN_ENV,
|
||||||
|
SYSTEMD.joinpath("KlipperScreen.service"),
|
||||||
|
]
|
||||||
|
status = get_install_status(KLIPPERSCREEN_DIR, files)
|
||||||
|
return {
|
||||||
|
"status": status.get("status"),
|
||||||
|
"status_code": status.get("status_code"),
|
||||||
|
"repo": get_repo_name(KLIPPERSCREEN_DIR),
|
||||||
|
"local": get_local_commit(KLIPPERSCREEN_DIR),
|
||||||
|
"remote": get_remote_commit(KLIPPERSCREEN_DIR),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def remove_klipperscreen() -> None:
|
||||||
|
Logger.print_status("Removing KlipperScreen ...")
|
||||||
|
try:
|
||||||
|
if KLIPPERSCREEN_DIR.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen directory ...")
|
||||||
|
shutil.rmtree(KLIPPERSCREEN_DIR)
|
||||||
|
Logger.print_ok("KlipperScreen directory successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("KlipperScreen directory not found!")
|
||||||
|
|
||||||
|
if KLIPPERSCREEN_ENV.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen environment ...")
|
||||||
|
shutil.rmtree(KLIPPERSCREEN_ENV)
|
||||||
|
Logger.print_ok("KlipperScreen environment successfully removed!")
|
||||||
|
else:
|
||||||
|
Logger.print_warn("KlipperScreen environment not found!")
|
||||||
|
|
||||||
|
service = SYSTEMD.joinpath("KlipperScreen.service")
|
||||||
|
if service.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen service ...")
|
||||||
|
control_systemd_service(service, "stop")
|
||||||
|
control_systemd_service(service, "disable")
|
||||||
|
remove_with_sudo(service)
|
||||||
|
Logger.print_ok("KlipperScreen service successfully removed!")
|
||||||
|
|
||||||
|
logfile = Path("/tmp/KlipperScreen.log")
|
||||||
|
if logfile.exists():
|
||||||
|
Logger.print_status("Removing KlipperScreen log file ...")
|
||||||
|
remove_with_sudo(logfile)
|
||||||
|
Logger.print_ok("KlipperScreen log file successfully removed!")
|
||||||
|
|
||||||
|
kl_im = InstanceManager(Klipper)
|
||||||
|
kl_instances: List[Klipper] = kl_im.instances
|
||||||
|
for instance in kl_instances:
|
||||||
|
logfile = instance.log_dir.joinpath("KlipperScreen.log")
|
||||||
|
if logfile.exists():
|
||||||
|
Logger.print_status(f"Removing {logfile} ...")
|
||||||
|
Path(logfile).unlink()
|
||||||
|
Logger.print_ok(f"{logfile} successfully removed!")
|
||||||
|
|
||||||
|
mr_im = InstanceManager(Moonraker)
|
||||||
|
mr_instances: List[Moonraker] = mr_im.instances
|
||||||
|
if mr_instances:
|
||||||
|
Logger.print_status("Removing KlipperScreen from update manager ...")
|
||||||
|
remove_config_section("update_manager KlipperScreen", mr_instances)
|
||||||
|
Logger.print_ok("KlipperScreen successfully removed from update manager!")
|
||||||
|
|
||||||
|
Logger.print_ok("KlipperScreen successfully removed!")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
Logger.print_error(f"Error removing KlipperScreen:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
def backup_klipperscreen_dir() -> None:
|
||||||
|
bm = BackupManager()
|
||||||
|
bm.backup_directory(
|
||||||
|
"KlipperScreen", source=KLIPPERSCREEN_DIR, target=KLIPPERSCREEN_BACKUP_DIR
|
||||||
|
)
|
||||||
|
bm.backup_directory(
|
||||||
|
"KlipperScreen-env", source=KLIPPERSCREEN_ENV, target=KLIPPERSCREEN_BACKUP_DIR
|
||||||
|
)
|
||||||
@@ -11,6 +11,7 @@ import textwrap
|
|||||||
from typing import Type, Optional
|
from typing import Type, Optional
|
||||||
|
|
||||||
from components.klipper.klipper_utils import backup_klipper_dir
|
from components.klipper.klipper_utils import backup_klipper_dir
|
||||||
|
from components.klipperscreen.klipperscreen import backup_klipperscreen_dir
|
||||||
from components.moonraker.moonraker_utils import (
|
from components.moonraker.moonraker_utils import (
|
||||||
backup_moonraker_dir,
|
backup_moonraker_dir,
|
||||||
backup_moonraker_db_dir,
|
backup_moonraker_db_dir,
|
||||||
@@ -104,4 +105,4 @@ class BackupMenu(BaseMenu):
|
|||||||
backup_client_config_data(FluiddData())
|
backup_client_config_data(FluiddData())
|
||||||
|
|
||||||
def backup_klipperscreen(self, **kwargs):
|
def backup_klipperscreen(self, **kwargs):
|
||||||
pass
|
backup_klipperscreen_dir()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Type, Optional
|
|||||||
|
|
||||||
from components.crowsnest.crowsnest import install_crowsnest
|
from components.crowsnest.crowsnest import install_crowsnest
|
||||||
from components.klipper import klipper_setup
|
from components.klipper import klipper_setup
|
||||||
|
from components.klipperscreen.klipperscreen import install_klipperscreen
|
||||||
from components.moonraker import moonraker_setup
|
from components.moonraker import moonraker_setup
|
||||||
from components.webui_client import client_setup
|
from components.webui_client import client_setup
|
||||||
from components.webui_client.client_config import client_config_setup
|
from components.webui_client.client_config import client_config_setup
|
||||||
@@ -45,6 +46,7 @@ class InstallMenu(BaseMenu):
|
|||||||
"4": Option(method=self.install_fluidd, menu=False),
|
"4": Option(method=self.install_fluidd, menu=False),
|
||||||
"5": Option(method=self.install_mainsail_config, menu=False),
|
"5": Option(method=self.install_mainsail_config, menu=False),
|
||||||
"6": Option(method=self.install_fluidd_config, menu=False),
|
"6": Option(method=self.install_fluidd_config, menu=False),
|
||||||
|
"7": Option(method=self.install_klipperscreen, menu=False),
|
||||||
"9": Option(method=self.install_crowsnest, menu=False),
|
"9": Option(method=self.install_crowsnest, menu=False),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,5 +93,8 @@ class InstallMenu(BaseMenu):
|
|||||||
def install_fluidd_config(self, **kwargs):
|
def install_fluidd_config(self, **kwargs):
|
||||||
client_config_setup.install_client_config(FluiddData())
|
client_config_setup.install_client_config(FluiddData())
|
||||||
|
|
||||||
|
def install_klipperscreen(self, **kwargs):
|
||||||
|
install_klipperscreen()
|
||||||
|
|
||||||
def install_crowsnest(self, **kwargs):
|
def install_crowsnest(self, **kwargs):
|
||||||
install_crowsnest()
|
install_crowsnest()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Type, Optional
|
|||||||
|
|
||||||
from components.crowsnest.crowsnest import get_crowsnest_status
|
from components.crowsnest.crowsnest import get_crowsnest_status
|
||||||
from components.klipper.klipper_utils import get_klipper_status
|
from components.klipper.klipper_utils import get_klipper_status
|
||||||
|
from components.klipperscreen.klipperscreen import get_klipperscreen_status
|
||||||
from components.log_uploads.menus.log_upload_menu import LogUploadMenu
|
from components.log_uploads.menus.log_upload_menu import LogUploadMenu
|
||||||
from components.moonraker.moonraker_utils import get_moonraker_status
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
from components.webui_client.client_utils import (
|
from components.webui_client.client_utils import (
|
||||||
@@ -91,6 +92,7 @@ class MainMenu(BaseMenu):
|
|||||||
self.ms_status = get_client_status(MainsailData())
|
self.ms_status = get_client_status(MainsailData())
|
||||||
self.fl_status = get_client_status(FluiddData())
|
self.fl_status = get_client_status(FluiddData())
|
||||||
self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
|
self.cc_status = get_current_client_config([MainsailData(), FluiddData()])
|
||||||
|
self._update_status("ks", get_klipperscreen_status)
|
||||||
self._update_status("cn", get_crowsnest_status)
|
self._update_status("cn", get_crowsnest_status)
|
||||||
|
|
||||||
def _update_status(self, status_name: str, status_fn: callable) -> None:
|
def _update_status(self, status_name: str, status_fn: callable) -> None:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Type, Optional
|
|||||||
|
|
||||||
from components.crowsnest.crowsnest import remove_crowsnest
|
from components.crowsnest.crowsnest import remove_crowsnest
|
||||||
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
|
from components.klipper.menus.klipper_remove_menu import KlipperRemoveMenu
|
||||||
|
from components.klipperscreen.klipperscreen import remove_klipperscreen
|
||||||
from components.moonraker.menus.moonraker_remove_menu import (
|
from components.moonraker.menus.moonraker_remove_menu import (
|
||||||
MoonrakerRemoveMenu,
|
MoonrakerRemoveMenu,
|
||||||
)
|
)
|
||||||
@@ -43,6 +44,7 @@ class RemoveMenu(BaseMenu):
|
|||||||
"2": Option(method=self.remove_moonraker, menu=True),
|
"2": Option(method=self.remove_moonraker, menu=True),
|
||||||
"3": Option(method=self.remove_mainsail, menu=True),
|
"3": Option(method=self.remove_mainsail, menu=True),
|
||||||
"4": Option(method=self.remove_fluidd, menu=True),
|
"4": Option(method=self.remove_fluidd, menu=True),
|
||||||
|
"5": Option(method=self.remove_klipperscreen, menu=True),
|
||||||
"6": Option(method=self.remove_crowsnest, menu=True),
|
"6": Option(method=self.remove_crowsnest, menu=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,5 +83,8 @@ class RemoveMenu(BaseMenu):
|
|||||||
def remove_fluidd(self, **kwargs):
|
def remove_fluidd(self, **kwargs):
|
||||||
ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run()
|
ClientRemoveMenu(previous_menu=self.__class__, client=FluiddData()).run()
|
||||||
|
|
||||||
|
def remove_klipperscreen(self, **kwargs):
|
||||||
|
remove_klipperscreen()
|
||||||
|
|
||||||
def remove_crowsnest(self, **kwargs):
|
def remove_crowsnest(self, **kwargs):
|
||||||
remove_crowsnest()
|
remove_crowsnest()
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ from components.klipper.klipper_setup import update_klipper
|
|||||||
from components.klipper.klipper_utils import (
|
from components.klipper.klipper_utils import (
|
||||||
get_klipper_status,
|
get_klipper_status,
|
||||||
)
|
)
|
||||||
|
from components.klipperscreen.klipperscreen import (
|
||||||
|
update_klipperscreen,
|
||||||
|
get_klipperscreen_status,
|
||||||
|
)
|
||||||
from components.moonraker.moonraker_setup import update_moonraker
|
from components.moonraker.moonraker_setup import update_moonraker
|
||||||
from components.moonraker.moonraker_utils import get_moonraker_status
|
from components.moonraker.moonraker_utils import get_moonraker_status
|
||||||
from components.webui_client.client_config.client_config_setup import (
|
from components.webui_client.client_config.client_config_setup import (
|
||||||
@@ -58,6 +62,8 @@ class UpdateMenu(BaseMenu):
|
|||||||
self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
self.mc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
self.fc_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
self.fc_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.ks_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
self.ks_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
self.cn_local = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
self.cn_remote = f"{COLOR_WHITE}{RESET_FORMAT}"
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ class UpdateMenu(BaseMenu):
|
|||||||
| 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} |
|
| 6) Fluidd-Config | {self.fc_local:<22} | {self.fc_remote:<22} |
|
||||||
| | | |
|
| | | |
|
||||||
| Other: |---------------|---------------|
|
| Other: |---------------|---------------|
|
||||||
| 7) KlipperScreen | | |
|
| 7) KlipperScreen | {self.ks_local:<22} | {self.ks_remote:<22} |
|
||||||
| 8) Mobileraker | | |
|
| 8) Mobileraker | | |
|
||||||
| 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} |
|
| 9) Crowsnest | {self.cn_local:<22} | {self.cn_remote:<22} |
|
||||||
| |-------------------------------|
|
| |-------------------------------|
|
||||||
@@ -142,7 +148,8 @@ class UpdateMenu(BaseMenu):
|
|||||||
def update_fluidd_config(self, **kwargs):
|
def update_fluidd_config(self, **kwargs):
|
||||||
update_client_config(self.fluidd_client)
|
update_client_config(self.fluidd_client)
|
||||||
|
|
||||||
def update_klipperscreen(self, **kwargs): ...
|
def update_klipperscreen(self, **kwargs):
|
||||||
|
update_klipperscreen()
|
||||||
|
|
||||||
def update_mobileraker(self, **kwargs): ...
|
def update_mobileraker(self, **kwargs): ...
|
||||||
|
|
||||||
@@ -193,6 +200,13 @@ class UpdateMenu(BaseMenu):
|
|||||||
)
|
)
|
||||||
self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}"
|
self.fc_remote = f"{COLOR_GREEN}{fc_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
|
# klipperscreen
|
||||||
|
ks_status = get_klipperscreen_status()
|
||||||
|
self.ks_local = self.format_local_status(
|
||||||
|
ks_status.get("local"), ks_status.get("remote")
|
||||||
|
)
|
||||||
|
self.ks_remote = f"{COLOR_GREEN}{ks_status.get('remote')}{RESET_FORMAT}"
|
||||||
|
|
||||||
# crowsnest
|
# crowsnest
|
||||||
cn_status = get_crowsnest_status()
|
cn_status = get_crowsnest_status()
|
||||||
self.cn_local = self.format_local_status(
|
self.cn_local = self.format_local_status(
|
||||||
|
|||||||
Reference in New Issue
Block a user