diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 905c8bbe4..000000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 160 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 098078dab..c121d8a1b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -96,6 +96,7 @@ repos: # Errors - "--select=PLE" - "--select=F" + - "--select=E" # Syntax error - "--select=E999" # Mostly have auto fixes available @@ -105,12 +106,18 @@ repos: - "--select=E714" - "--select=E711" - "--select=E713" + - "--select=FURB" # Autofixable quote rules - "--select=Q" - "--select=E722" # Fix bad escape sequence - "--select=W605" + # Import + - "--select=I" + # Bandit + #- "--select=S" + # Run the formatter. - id: ruff-format diff --git a/Makefile b/Makefile index 6fa0e20ce..17c1b41c7 100644 --- a/Makefile +++ b/Makefile @@ -69,36 +69,6 @@ ${ROOT_DIR}/.isolated_venv: # Create the virtualenv in the project folder update: # Fetch new code into this project folder git pull -.PHONY: dev-make-venv -dev-make-venv: ${ROOT_DIR}/.venv ${ROOT_DIR}/.isolated_venv # Make the virtualenv in this project folder. - @echo "Making venv if not present" - -.PHONY: dev-install -dev-install: dev-make-venv # Install Kaithem and all it's dependencies in the Venv. - @cd ${ROOT_DIR} - @.venv/bin/python -m pip install --upgrade -r requirements.txt - @.venv/bin/python -m pip install --editable . - -.PHONY: dev-run -dev-run: # Run the kaithem app. - @cd ${ROOT_DIR} - @pw-jack .venv/bin/python -m kaithem - -.PHONY: dev-run-isolated -dev-run-isolated: # Run the kaithem app. - @cd ${ROOT_DIR} - @pw-jack .isolated_venv/bin/python -m kaithem - -.PHONY: dev-update-dependencies -dev-update-dependencies: dev-make-venv # Install latest version of dependencies into the venv. New versions might break something! - @cd ${ROOT_DIR} - @.isolated_venv/bin/python -m pip install --upgrade -r direct_dependencies.txt - @.isolated_venv/bin/python -m pip freeze -l > requirements.txt - # If kaithem itself installed here, avoid circular nonsense - @sed -i '/.*kaithem.*/d' ./requirements.txt - @.venv/bin/python -m pip install --force --upgrade -r requirements.txt - - .PHONY: root-install-zrok root-install-zrok: # Install or update Zrok for remote access @@ -120,10 +90,10 @@ user-set-global-pipewire-conf: @systemctl --user restart pipewire wireplumber .PHONY: user-install-kaithem -user-install-kaithem: # Install kaithem to run as your user. Note that it only runs when you are logged in. +user-start-kaithem-at-boot: # Install kaithem to run as your user. Note that it only runs when you are logged in. @cd ${ROOT_DIR} - @echo "Kaithem will be installed to /home/${USER}/kaithem/.venv" - @bash ./scripts/install-kaithem.sh + @echo "Kaithem will be installed with a systemd user service." + @bash ./scripts/install-kaithem-service.sh .PHONY: user-max-volume-at-boot user-max-volume-at-boot: #Install a service that sets the max volume when you log in. @@ -150,7 +120,7 @@ user-kaithem-status: # Get the status of the running kaithem instance .PHONY: root-install-system-dependencies root-install-system-dependencies: # Install non-python libraries using apt - @sudo apt install python3-virtualenv scrot mpv lm-sensors python3-netifaces python3-gst-1.0 gstreamer1.0-plugins-good gstreamer1.0-plugins-bad swh-plugins tap-plugins caps gstreamer1.0-plugins-ugly fluidsynth libfluidsynth3 gstreamer1.0-pocketsphinx x42-plugins gstreamer1.0-opencv gstreamer1.0-vaapi python3-opencv gstreamer1.0-pipewire + @sudo apt install python3-virtualenv pipx scrot mpv lm-sensors python3-netifaces python3-gst-1.0 gstreamer1.0-plugins-good gstreamer1.0-plugins-bad swh-plugins tap-plugins caps gstreamer1.0-plugins-ugly fluidsynth libfluidsynth3 gstreamer1.0-pocketsphinx x42-plugins gstreamer1.0-opencv gstreamer1.0-vaapi python3-opencv gstreamer1.0-pipewire .PHONY: root-use-pipewire-jack root-use-pipewire-jack: # Make JACK clients work with pipewire diff --git a/README.md b/README.md index 5b393945f..f1981a445 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ ![Makefile Badge](badges/makefile.png) ![Ten Year Project](badges/ten-years.png) ![Pytest](badges/pytest.png) +![Ruff](badges/ruff.png) +![Poetry](badges/poetry.png) Kaithem is Linux home/commercial automation server written in pure Python(3.10 and up). Not tested outside of Linux. Resource usage is low enough to run well on the Raspberry Pi. @@ -30,6 +32,8 @@ cd KaithemAutomation Now you have the repo cloned, all the relevant commands are in the Makefile. This is an interpreted package, but we use Make anyway to keep commands in one handy place. +NOTE: Formerly, the makefile would use a script to create a virtual environment, now we let pipx +handle it all. ### Install system packages @@ -42,15 +46,27 @@ make root-install-system-dependencies ### Install kaithem in the project folder virtualenv + +Now that you have the system dependencies, you should have pipx from your package manager. + ```bash -# Show the menu of Kaithem commands -make help -# Grab Pip dependencies and install into this cloned project folder -make dev-install +# Kaithem now uses Poetry as a Python builder +pipx install poetry + +# This line tells Poetry that Kaithem should use your +# globally installed system packages. This is important +# Because GStreamer is normally installed that way + +poetry config virtualenvs.options.system-site-packages true --local + +# If you already have a .venv in your folder, it +# May be best to start over. +poetry install -v + +# Poetry will run it in the virtualenv +poetry run python dev_run.py -# Run the file(Launches dev_run in a virtualenv) -make dev-run ``` Then visit http://localhost:8002 and log in with your normal Linux username and password. diff --git a/badges/poetry.png b/badges/poetry.png new file mode 100644 index 000000000..c357cca60 Binary files /dev/null and b/badges/poetry.png differ diff --git a/default.nix b/default.nix deleted file mode 100644 index 5baefc813..000000000 --- a/default.nix +++ /dev/null @@ -1,122 +0,0 @@ -{ pkgs ? import {} }: -let -pyjack = with pkgs.python310Packages; ( - buildPythonPackage rec { - pname = "JACK-Client"; - version = "0.5.4"; - format="wheel"; - - src = pkgs.fetchurl { - url="https://files.pythonhosted.org/packages/17/41/de1269065ff0d1bda143cc91b245aef3165d9259e27904a4a59eab081e0b/JACK_Client-0.5.4-py3-none-any.whl"; - sha256 = "sha256-UsphZEONO3+M/azl9vqzxyJP5jAi78hccCmGLUNbznM="; - }; - doCheck = false; - propagatedBuildInputs = [ - # Specify dependencies - pkgs.python310Packages.numpy - pkgs.python310Packages.cffi - pkgs.libjack2 - pkgs.jack2 - ]; - } - ); -in - -with pkgs.python310Packages; -(buildPythonApplication { - pname = "kaithem"; - version = "0.64.3"; - - propagatedBuildInputs = with pkgs; [ - gtk3 # Pango - gobject-introspection # Pango - - gst_all_1.gstreamer - #gst_all_1.gstreamer.dev # gst-inspect - gst_all_1.gst-plugins-base # playbin - (gst_all_1.gst-plugins-good.override { gtkSupport = true; }) # gtksink - gst_all_1.gst-plugins-bad - gst_all_1.gst-plugins-ugly - gst_all_1.gst-libav - python310Packages.gst-python - jack2 - libjack2 - pyjack - python310Packages.numpy - - mpv - networkmanager - python310Packages.python-rtmidi - python310Packages.pillow - python310Packages.paho-mqtt - python310Packages.msgpack - python310Packages.python-pam - python310Packages.scikit-image - python310Packages.pyserial - python310Packages.netifaces - python310Packages.psutil - python310Packages.evdev - python310Packages.setproctitle - fluidsynth - ( - buildPythonPackage rec { - pname = "tflite-runtime"; - version = "2.13.0"; - format="wheel"; - - src = fetchurl { - url=if pkgs.system == "x86_64-linux" - then "https://files.pythonhosted.org/packages/93/b3/0c6d6a58e67f9af035863b82ef09926eacc2ab43b2eb537cb345c53b4c1e/tflite_runtime-2.13.0-cp310-cp310-manylinux2014_x86_64.whl" - else "https://files.pythonhosted.org/packages/ac/01/ac170459779f503581c492d65d1d339d223ac09a7e92f379eddc689678ec/tflite_runtime-2.13.0-cp310-cp310-manylinux2014_aarch64.whl"; - - sha256 = if pkgs.system == "x86_64-linux" - then "sha256-bmAIzUsLrKlHrwu8KxhQlvF/jm/EbH5vr/vQJvzF2K8=" - else ""; - }; - doCheck = false; - propagatedBuildInputs = [ - # Specify dependencies - pkgs.python310Packages.numpy - ]; - } - ) - - - - ]; - doCheck = false; - src = ./.; - - #ldconfig from iconv is required to make _find_path in Python Ctypes work - postFixup = '' - wrapProgram $out/bin/kaithem \ - --set PATH ${lib.makeBinPath (with pkgs; [ - mpv - networkmanager - iconv - coreutils - bash - busybox - binutils_nogold - (python311.withPackages(ps: with ps; [ pyjack])) - ])} - - wrapProgram $out/bin/kaithem._jackmanager_server \ - --set PATH ${lib.makeBinPath (with pkgs; [ - iconv - coreutils - bash - busybox - binutils_nogold - ])} - - wrapProgram $out/bin/kaithem._iceflow_server \ - --set PATH ${lib.makeBinPath (with pkgs; [ - iconv - coreutils - bash - busybox - binutils_nogold - ])} -''; -}) \ No newline at end of file diff --git a/direct_dependencies.txt b/direct_dependencies.txt index c0483ca98..35231dcdd 100644 --- a/direct_dependencies.txt +++ b/direct_dependencies.txt @@ -1,82 +1,82 @@ -# This file is maintained manually. It only has the top-level dependencies so pip -# Can manage the rest. You should probably be installing requirements.txt instead # First party -scullery>=0.16.0 -iot-devices -icemedia -NVRChannel +scullery = "^0.1.16" +iot-devices= "*" +icemedia= "*" +NVRChannel= "*" -numpy>=1.26.1 -cherrypy -cheroot -flask -tornado -mako -jinja2 -astral -tatsu -pam -msgpack -pyyaml -types-PyYaml -pytest -nmcli -peewee -terminado -apprise +# NVRChannel didn't bring this in 0.15.0 +opencv-python= "*" -ffmpeg-python -yappi -zeroconf>=0.119.0 -colorzero +numpy="^1.26.1" +cherrypy = "*" +cheroot= "*" +flask= "*" +tornado= "*" +mako= "*" +jinja2= "*" +astral= "*" +tatsu= "*" +pam= "*" +msgpack= "*" +pyyaml= "*" +types-PyYaml= "*" +pytest= "*" +nmcli= "*" +peewee= "*" +terminado= "*" +apprise= "*" + +ffmpeg-python= "*" +yappi= "*" +zeroconf="^0.119.0" +colorzero= "*" # Still included in repo because it doesn't install correctly due to # suds-passworddigest #onvif -typeguard -tinytag -jsonschema -pint -pyflakes -python_mpv_jsonipc -textdistance -toml -vignette -simpleeval -websockets -zeep -passlib -jinja2 -Pillow -tflite-runtime -evdev -attr -markupsafe -upnpclient -requests -types-requests -python-dateutil -pygments -pytz -ntplib -holidays - +typeguard= "*" +tinytag= "*" +jsonschema= "*" +pint= "*" +pyflakes= "*" +python_mpv_jsonipc= "*" +textdistance= "*" +toml= "*" +vignette= "*" +simpleeval= "*" +websockets= "*" +zeep= "*" +passlib= "*" +jinja2= "*" +Pillow= "*" +tflite-runtime= "*" +evdev= "*" +attr= "*" +markupsafe= "*" +upnpclient= "*" +requests= "*" +types-requests= "*" +python-dateutil= "*" +pygments= "*" +pytz= "*" +ntplib= "*" +holidays= "*" -yeelight +yeelight= "*" -pyserial -pygrep -python-rtmidi==1.5.0 -paho-mqtt<=1.6.0 -setproctitle -psutil -toml -netifaces -JACK-Client -aioesphomeapi -sf2utils -pynput +pyserial= "*" +pygrep= "*" +python-rtmidi= "1.5.0" +paho-mqtt = "<=1.6.0" +setproctitle= "*" +psutil= "*" +toml= "*" +netifaces= "*" +JACK-Client= "*" +aioesphomeapi= "*" +sf2utils= "*" +pynput= "*" # Older is not compatible with new numpy -scipy>=1.11.0 \ No newline at end of file +scipy = ">=1.11.0" \ No newline at end of file diff --git a/kaithem/data/config-schema.yaml b/kaithem/data/config-schema.yaml index 5deaea594..fdb0ce703 100644 --- a/kaithem/data/config-schema.yaml +++ b/kaithem/data/config-schema.yaml @@ -20,12 +20,12 @@ properties: type: string default: "default" description: "IP to bind to. Default all interfaces unless local-access-only set" - + location: type: string default: "" description: "Comma delimited server location default value, can be changed by user" - + ssl-dir: type: string @@ -141,11 +141,11 @@ properties: log-dump-size: type: integer minimum: 1 - + log-buffer: type: integer minimum: 1 - + keep-log-files: type: string pattern: "[0-9]*(k|m|g|K|M|G)" @@ -211,9 +211,6 @@ properties: theme-url: type: string - monaco-theme-url: - type: string - quotes-file: type: string diff --git a/kaithem/data/default_configuration.yaml b/kaithem/data/default_configuration.yaml index f2fbdee42..95840a55d 100644 --- a/kaithem/data/default_configuration.yaml +++ b/kaithem/data/default_configuration.yaml @@ -18,9 +18,7 @@ host: "default" ssl-dir : ssl #The directory in which to store persistant state such as users,groups,modules,settings,etc -#Relative to kaithem/ or use an absolute path. -# gets auto-overridden to ~/kaithem if we detect that it is in a path under /usr or /nix -site-data-dir: var +site-data-dir: ~/kaithem #Max how many threads to allocate for the worker thread pool worker-threads: 12 @@ -169,10 +167,6 @@ about-box-banner: "This space can be customized with site specific data in the c #theme-url: /static/css/kaithem_scrapbook.css theme-url: /static/css/kaithem_minimal.css -#URL of a monaco theme to use -monaco-theme-url: /static/js/monaco-theme.js - - #How to format times and dates time-format: "%A, %B %d, %Y at %I:%M:%S %p Server Time" diff --git a/kaithem/main.py b/kaithem/main.py deleted file mode 100644 index 76edab75f..000000000 --- a/kaithem/main.py +++ /dev/null @@ -1,113 +0,0 @@ -#This is the kivy android app. -#IT DOESN'T WORK ON NON-ANDROID!!!! Use kaithem.py - -from kivy.app import App -from kivy.lang import Builder -from kivy.utils import platform -from kivy.uix.button import Button -from kivy.uix.label import Label - -from kivy.uix.boxlayout import BoxLayout -import threading -from kivy.uix.screenmanager import ScreenManager, Screen - -import time,sys - -class ServiceApp(App): - - def stop_service(self,foo=None): - if self.service: - self.service.stop() - self.service = None - else: - sys.exit() - - def start_service(self,foo=None): - if self.service: - self.service.stop() - self.service = None - - if platform == 'android': - from android import AndroidService - service = AndroidService('KaithemAutomation', 'running') - service.start('service started') - self.service = service - else: - def f(): - from kaithem.src import main - t = threading.Thread(target=f,daemon=True) - t.start() - - def build(self): - self.service=None - - self.start_service() - - # Create the manager - sm = ScreenManager() - sm.add_widget(self.makeMainScreen()) - - self.screenManager = sm - return sm - - - - def makeMainScreen(self): - mainScreen = Screen(name='Main') - - - layout = BoxLayout(orientation='vertical') - mainScreen.add_widget(layout) - label = Label(text='KaithemAutomation Service Controller') - layout.add_widget(label) - - - btn2 = Button(text='Go to the GUI') - btn2.bind(on_press=self.gotoGui) - - btn3 = Button(text='Stop the service') - btn3.bind(on_press=self.stop_service) - - btn4 = Button(text='Start or restart.') - btn4.bind(on_press=self.start_service) - - layout.add_widget(btn2) - layout.add_widget(btn3) - layout.add_widget(btn4) - - return mainScreen - - - - - def gotoGui(self): - self.openInBrowser("http://localhost:8002") - - - def openInBrowser(self,link): - "Opens a link in the browser" - if platform == 'android': - from jnius import autoclass,cast - # import the needed Java class - PythonActivity = autoclass('org.renpy.kivy.PythonActivity') - Intent = autoclass('android.content.Intent') - Uri = autoclass('android.net.Uri') - - # create the intent - intent = Intent() - intent.setAction(Intent.ACTION_VIEW) - intent.setData(Uri.parse(link)) - - # PythonActivity.mActivity is the instance of the current Activity - # BUT, startActivity is a method from the Activity class, not from our - # PythonActivity. - # We need to cast our class into an activity and use it - currentActivity = cast('android.app.Activity', PythonActivity.mActivity) - currentActivity.startActivity(intent) - else: - import webbrowser - webbrowser.open(link) - - -if __name__ == '__main__': - ServiceApp().run() diff --git a/kaithem/src/builtintags.py b/kaithem/src/builtintags.py index d8a9d9fb1..55a23b9c4 100644 --- a/kaithem/src/builtintags.py +++ b/kaithem/src/builtintags.py @@ -1,12 +1,12 @@ -import logging -from kaithem.src import tagpoints, alerts, messagebus, geolocation -import traceback -import time import json +import logging import os +import time +import traceback -from . import astrallibwrapper as sky +from kaithem.src import alerts, geolocation, messagebus, tagpoints +from . import astrallibwrapper as sky refs = [] @@ -44,16 +44,12 @@ def civil_twilight(): twilightTag.min = -1 twilightTag.max = 1 twilightTag.interval = 60 - twilightTag.description = ( - "Unless overridden, 1 if dark, else 0, -1 if no location is set" - ) + twilightTag.description = "Unless overridden, 1 if dark, else 0, -1 if no location is set" twilightTag.value = civil_twilight refs.append(twilightTag) alertTag = tagpoints.Tag("/system/alerts.level") - alertTag.description = ( - "The level of the highest priority alert that is currently not acknowledged" - ) + alertTag.description = "The level of the highest priority alert that is currently not acknowledged" alertTag.writable = False alertTag.min = 0 alertTag.max = alerts.priorities["critical"] @@ -130,4 +126,4 @@ def publicIP(): # Probably best not to automatically do anything that could cause IP traffic? -# ipTag.setAlarm("NoInternetAccess", condition="not value") +# ipTag.set_alarm("NoInternetAccess", condition="not value") diff --git a/kaithem/src/config.py b/kaithem/src/config.py index 856ce7d4b..ec6b84ba7 100644 --- a/kaithem/src/config.py +++ b/kaithem/src/config.py @@ -3,13 +3,14 @@ """This file handles the big configuration file, provides access to it, and handles default settings""" -import yaml import argparse -import sys +import logging import os +import sys +from typing import Any, Dict, Optional + import jsonschema -import logging -from typing import Optional, Dict, Any +import yaml logger = logging.getLogger("system") config = {} @@ -66,11 +67,20 @@ def load(cfg: Dict[str, Any]): config.update(cfg or {}) + vardir = os.path.expanduser(config["site-data-dir"]) + default_conf_location = os.path.join(vardir, "config.yaml") + # Attempt to open any manually specified config file if argcmd.c: with open(argcmd.c) as f: _usr_config = yaml.load(f, yaml.SafeLoader) logger.info("Loaded configuration from " + argcmd.c) + + elif os.path.exists(default_conf_location): + with open(default_conf_location) as f: + _usr_config = yaml.load(f, yaml.SafeLoader) + logger.info("Loaded configuration from " + default_conf_location) + else: _usr_config = {} logger.info("No CFG File Specified. Using Defaults.") diff --git a/kaithem/src/devices.py b/kaithem/src/devices.py index c9b5c0f56..248e05feb 100644 --- a/kaithem/src/devices.py +++ b/kaithem/src/devices.py @@ -1,28 +1,37 @@ from __future__ import annotations -# SPDX-FileCopyrightText: Copyright 2018 Daniel Dunn -# SPDX-License-Identifier: GPL-3.0-only -from . import modules_state -from .modules_state import additionalTypes, ResourceType -import weakref -import time -import textwrap -import logging -import traceback +import copy import gc +import logging import os -import cherrypy -import cherrypy.lib.static -import copy import shutil -from typing import Any +import textwrap +import time +import traceback +import weakref from collections.abc import Callable, Iterable +from typing import Any -from . import pages, workers, tagpoints, alerts -from . import persist, directories, messagebus, widgets, unitsofmeasure - -import iot_devices.host +import cherrypy +import cherrypy.lib.static import iot_devices.device +import iot_devices.host + +# SPDX-FileCopyrightText: Copyright 2018 Daniel Dunn +# SPDX-License-Identifier: GPL-3.0-only +from . import ( + alerts, + directories, + messagebus, + modules_state, + pages, + persist, + tagpoints, + unitsofmeasure, + widgets, + workers, +) +from .modules_state import ResourceType, additionalTypes SUBDEVICE_SEPARATOR = "/" @@ -99,18 +108,14 @@ def delete_bookkeep(name, confdir=False): if confdir: try: - old_dev_conf_folder = get_config_folder_from_info( - pm, pr, name, create=False, always_return=True - ) + old_dev_conf_folder = get_config_folder_from_info(pm, pr, name, create=False, always_return=True) if old_dev_conf_folder and os.path.isdir(old_dev_conf_folder): if not old_dev_conf_folder.count("/") > 3: # Basically since rmtree is so dangerous we make sure # it absolutely cannot be any root or nearly root level folder # in the user's home dir even if some unknown future error happens. # I have no reason to think this will ever actually be needed. - raise RuntimeError( - f"Defensive check failed: {old_dev_conf_folder}" - ) + raise RuntimeError(f"Defensive check failed: {old_dev_conf_folder}") shutil.rmtree(old_dev_conf_folder) except Exception: @@ -204,13 +209,8 @@ def load_closure(): if cls: return else: - if ( - not value["device"]["type"] - == remote_devices[devname].device_type_name - ): - raise RuntimeError( - "Name in user, can't overwrite this device name with a different type" - ) + if not value["device"]["type"] == remote_devices[devname].device_type_name: + raise RuntimeError("Name in user, can't overwrite this device name with a different type") remote_devices[devname].close() d = makeDevice(devname, value["device"], cls) @@ -240,18 +240,14 @@ def create(self, module, path, name, kwargs): raise RuntimeError("Not implemented, devices uses it's own create page") def createpage(self, module, path): - return pages.get_template("devices/deviceintomodule.html").render( - module=module, path=path - ) + return pages.get_template("devices/deviceintomodule.html").render(module=module, path=path) def editpage(self, module, name, value): with modules_state.modulesLock: n = name.split(SUBDEVICE_SEPARATOR)[-1] if "name" in value["device"]: n = value["device"]["name"] - return pages.get_template("devices/device.html").render( - data=remote_devices[n].config, obj=remote_devices[n], name=n - ) + return pages.get_template("devices/device.html").render(data=remote_devices[n].config, obj=remote_devices[n], name=n) drt = DeviceResourceType() @@ -289,10 +285,7 @@ def saveDevice(d): def get_config_folder_from_device(d: str, create=True): - if ( - not hasattr(remote_devices[d], "parentModule") - or not remote_devices[d].parentModule - ): + if not hasattr(remote_devices[d], "parentModule") or not remote_devices[d].parentModule: module = None resource = None else: @@ -347,9 +340,7 @@ def f(): def makeBackgroundErrorFunction(t, time, self): # Don't block everything up def f(): - self.logWindow.write( - f'
Error at {time}
{t}
' - ) + self.logWindow.write(f'
Error at {time}
{t}
') return f @@ -364,7 +355,7 @@ class Device(iot_devices.device.Device): ###################################################################################### # Alarms are only done via the new tags way with these - _noSetAlarmPriority = True + _noset_alarmPriority = True _isCrossFramework = True @@ -393,28 +384,22 @@ def setDataKey(self, key, val): with modules_state.modulesLock: self.config[key] = v - if ( - not self.config.get("is_ephemeral", False) - and not key.startswith("temp.") - and not key.startswith("kaithem.temp.") - ): + if not self.config.get("is_ephemeral", False) and not key.startswith("temp.") and not key.startswith("kaithem.temp."): if self.parentModule: - modules_state.ActiveModules[self.parentModule][self.parentResource][ - "device" - ][key] = v + modules_state.ActiveModules[self.parentModule][self.parentResource]["device"][key] = v modules_state.saveResource( self.parentModule, self.parentResource, - modules_state.ActiveModules[self.parentModule][ - self.parentResource - ], + modules_state.ActiveModules[self.parentModule][self.parentResource], ) modules_state.modulesHaveChanged() else: - # This might not be stored in the master lists, and yet it might not be connected to + # This might not be stored in the master lists, + # and yet it might not be connected to # the parentModule, because of legacy API reasons. - # Just store it it self.config which will get saved at the end of makeDevice, that pretty much handles all module devices + # Just store it it self.config which will get saved + # at the end of makeDevice, that pretty much handles all module devices if self.name in device_data: device_data[self.name][key] = v saveDevice(self.name) @@ -434,15 +419,9 @@ def f(u, v): return f def __init__(self, name, data): - if ( - not data["type"] == self.device_type_name - and not self.device_type_name == "unsupported" - ): + if not data["type"] == self.device_type_name and not self.device_type_name == "unsupported": raise ValueError( - "Incorrect device type in info dict," - + data["type"] - + " does not match device_type_name " - + self.device_type_name + "Incorrect device type in info dict," + data["type"] + " does not match device_type_name " + self.device_type_name ) global remote_devices_atomic global remote_devices @@ -556,15 +535,9 @@ def handle_error(self, s): if len(self.errors) > 50: self.errors.pop(0) - workers.do( - makeBackgroundErrorFunction( - textwrap.fill(s, 120), unitsofmeasure.strftime(time.time()), self - ) - ) + workers.do(makeBackgroundErrorFunction(textwrap.fill(s, 120), unitsofmeasure.strftime(time.time()), self)) if len(self.errors) == 1: - messagebus.post_message( - "/system/notifications/errors", f"First error in device: {self.name}" - ) + messagebus.post_message("/system/notifications/errors", f"First error in device: {self.name}") syslogger.error(f"in device: {self.name}\n{s}") def onGenericUIMessage(self, u, v): @@ -574,9 +547,7 @@ def onGenericUIMessage(self, u, v): if v[0] == "fake": if v[2] is not None: - self.tagPoints[v[1]]._k_ui_fake = self.tagPoints[v[1]].claim( - v[2], "faked", priority=50.5 - ) + self.tagPoints[v[1]]._k_ui_fake = self.tagPoints[v[1]].claim(v[2], "faked", priority=50.5) else: if hasattr(self.tagPoints[v[1]], "_k_ui_fake"): @@ -626,9 +597,7 @@ def create_subdevice(self, cls, name: str, config: dict, *a, **k): Allows a device to create it's own subdevices. """ if self.config.get("is_subdevice", False): - raise RuntimeError( - "Kaithem does not support more than two layers of subdevice" - ) + raise RuntimeError("Kaithem does not support more than two layers of subdevice") global remote_devices_atomic @@ -998,9 +967,7 @@ def handle_exception(self): class UnsupportedDevice(iot_devices.device.Device): - description = ( - "This device does not have support, or else the support is not loaded." - ) + description = "This device does not have support, or else the support is not loaded." device_type_name = "unsupported" device_type = "unsupported" @@ -1042,10 +1009,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): with modules_state.modulesLock: if kwargs.get("temp.kaithem.store_in_module", None): - if ( - kwargs["temp.kaithem.store_in_module"] - not in modules_state.ActiveModules - ): + if kwargs["temp.kaithem.store_in_module"] not in modules_state.ActiveModules: raise ValueError("Can't store in nonexistant module") m = kwargs["temp.kaithem.store_in_module"] @@ -1053,9 +1017,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): if r in modules_state.ActiveModules[m]: if not modules_state.ActiveModules[m][r]["resource-type"] == "device": - raise ValueError( - "A resource in the module with that name exists and is not a device." - ) + raise ValueError("A resource in the module with that name exists and is not a device.") # Make sure we don't corrupt state by putting a folder where a file already is ensure_module_path_ok(m, r) @@ -1069,22 +1031,16 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): parentModule = remote_devices[devname].parentModule parentResource = remote_devices[devname].parentResource - old_dev_conf_folder = get_config_folder_from_info( - parentModule, parentResource, devname, create=False, always_return=True - ) + old_dev_conf_folder = get_config_folder_from_info(parentModule, parentResource, devname, create=False, always_return=True) if "temp.kaithem.store_in_module" in kwargs: newparentModule = kwargs["temp.kaithem.store_in_module"] - newparentResource = kwargs["temp.kaithem.store_in_resource"] or ".d/".join( - name.split("/") - ) + newparentResource = kwargs["temp.kaithem.store_in_resource"] or ".d/".join(name.split("/")) else: raise ValueError("Can only save in module") - new_dev_conf_folder = get_config_folder_from_info( - newparentModule, newparentResource, name, create=False, always_return=True - ) + new_dev_conf_folder = get_config_folder_from_info(newparentModule, newparentResource, name, create=False, always_return=True) if not parentModule: dt = device_data[devname] @@ -1101,9 +1057,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): 1, "1", ) - configuredAsSubdevice = ( - configuredAsSubdevice or dt.get("parent_device", "").strip() - ) + configuredAsSubdevice = configuredAsSubdevice or dt.get("parent_device", "").strip() old_read_perms = remote_devices[devname].config.get("kaithem.read_perms", []) @@ -1118,11 +1072,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): time.sleep(0.01) gc.collect() - savable_data = { - i: kwargs[i] - for i in kwargs - if ((not i.startswith("temp.")) and not i.startswith("filedata.")) - } + savable_data = {i: kwargs[i] for i in kwargs if ((not i.startswith("temp.")) and not i.startswith("filedata."))} # Propagate subdevice status even if it is just loaded as a placeholder if configuredAsSubdevice or subdevice: @@ -1143,17 +1093,13 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): if new_dev_conf_folder: if old_dev_conf_folder and os.path.exists(old_dev_conf_folder): os.makedirs(new_dev_conf_folder, exist_ok=True, mode=0o700) - shutil.copytree( - old_dev_conf_folder, new_dev_conf_folder, dirs_exist_ok=True - ) + shutil.copytree(old_dev_conf_folder, new_dev_conf_folder, dirs_exist_ok=True) if not old_dev_conf_folder.count("/") > 3: # Basically since rmtree is so dangerous we make sure # it absolutely cannot be any root or nearly root level folder # in the user's home dir even if some unknown future error happens. # I have no reason to think this will ever actually be needed. - raise RuntimeError( - f"Defensive check failed: {old_dev_conf_folder}" - ) + raise RuntimeError(f"Defensive check failed: {old_dev_conf_folder}") shutil.rmtree(old_dev_conf_folder) for i in fd: @@ -1182,11 +1128,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): kwargs["is_subdevice"] = "true" # Don't pass our special internal keys to that mechanism that expects to only see standard iot_devices keys. - k = { - i: kwargs[i] - for i in kwargs - if not i.startswith("filedata.") and not i.startswith("temp.kaithem.") - } + k = {i: kwargs[i] for i in kwargs if not i.startswith("filedata.") and not i.startswith("temp.kaithem.")} subdevice_data_cache[name] = savable_data device_location_cache[name] = newparentModule, newparentResource @@ -1209,9 +1151,7 @@ def updateDevice(devname, kwargs: dict[str, Any], saveChanges=True): remote_devices[name].parentResource = newparentResource if newparentModule: - storeDeviceInModule( - savable_data, newparentModule, newparentResource or name - ) + storeDeviceInModule(savable_data, newparentModule, newparentResource or name) else: # Allow name changing via data, we save under new, not the old name device_data[name] = savable_data @@ -1350,11 +1290,7 @@ def makeDevice(name, data, cls=None): # Don't pass framewith specific stuff to them. # Except a whitelist of known short string only keys that we need to easily access from # within the device integration code - new_data = { - i: new_data[i] - for i in new_data - if ((not i.startswith("temp.kaithem.")) and (not i.startswith("filedata."))) - } + new_data = {i: new_data[i] for i in new_data if ((not i.startswith("temp.kaithem.")) and (not i.startswith("filedata.")))} try: d = dt(name, new_data) @@ -1373,10 +1309,7 @@ def ensure_module_path_ok(module, resource): dir = "/".join(resource.split("/")[:-1]) for i in range(256): if dir in modules_state.ActiveModules[module]: - if ( - not modules_state.ActiveModules[module][dir]["resource-type"] - == "directory" - ): + if not modules_state.ActiveModules[module][dir]["resource-type"] == "directory": raise RuntimeError(f"File exists blocking creation of: {module}") if not dir.count("/"): break @@ -1410,9 +1343,7 @@ def storeDeviceInModule(d: dict, module: str, resource: str) -> None: "device": d, } - modules_state.saveResource( - module, resource, {"resource-type": "device", "device": d} - ) + modules_state.saveResource(module, resource, {"resource-type": "device", "device": d}) modules_state.modulesHaveChanged() @@ -1434,9 +1365,7 @@ def __init__(self, fn): self.fn = fn def __get__(self, instance, owner): - return lambda: pages.get_vardir_template(self.fn).render( - data=instance.config, obj=instance, name=instance.name - ) + return lambda: pages.get_vardir_template(self.fn).render(data=instance.config, obj=instance, name=instance.name) def setupSubdeviceData(): @@ -1486,10 +1415,7 @@ def createDevicesFromData(): cls = UnusedSubdevice # We can call this again to reload unsupported devices. - if ( - name in remote_devices - and not remote_devices[name].device_type_name == "unsupported" - ): + if name in remote_devices and not remote_devices[name].device_type_name == "unsupported": continue try: @@ -1530,9 +1456,7 @@ def warnAboutUnsupportedDevices(): f"Device {str(i)} not supported", ) except Exception: - syslogger.exception( - f"Error warning about missing device support device {str(i)}" - ) + syslogger.exception(f"Error warning about missing device support device {str(i)}") def init_devices(): diff --git a/kaithem/src/dialogs.py b/kaithem/src/dialogs.py index 173296dec..d00fefe6d 100644 --- a/kaithem/src/dialogs.py +++ b/kaithem/src/dialogs.py @@ -23,30 +23,59 @@ def is_disabled_by_default(self): def text(self, s: str): self.items.append(("", f"

{s}

")) - def text_input( - self, name: str, *, title: str | None = None, default: str = "", disabled=None - ): + def text_input(self, name: str, *, title: str | None = None, default: str = "", disabled=None): title = title or self.name_to_title(name) if disabled is None: disabled = self.is_disabled_by_default() disabled = " disabled" if disabled else "" + self.items.append((title, f'')) + + def checkbox(self, name: str, *, title: str | None = None, default: str = "", disabled=None): + title = title or self.name_to_title(name) + + if disabled is None: + disabled = self.is_disabled_by_default() + + disabled = " disabled" if disabled else "" + checked = "checked" if default else "" + + self.items.append((title, f'')) + + def selection(self, name: str, *, options: list[str], title: str | None = None, disabled=None): + title = title or self.name_to_title(name) + + if disabled is None: + disabled = self.is_disabled_by_default() + + disabled = " disabled" if disabled else "" + + options = options or [] + + o = "" + + for i in options: + o += f"\n" + self.items.append( - (title, f'') + ( + title, + f""" + + """, + ) ) - def submit_button( - self, name: str, *, title: str | None = None, value: str = "", disabled=None - ): + def submit_button(self, name: str, *, title: str | None = None, value: str = "", disabled=None): if disabled is None: disabled = self.is_disabled_by_default() title = title or "Submit" disabled = " disabled" if disabled else "" - self.items.append( - ("", f'') - ) + self.items.append(("", f'')) def render(self, target: str, hidden_inputs: dict | None = None): "The form will target the given URL and have all the keys and values in hidden inputs" diff --git a/kaithem/src/directories.py b/kaithem/src/directories.py index a58ed283f..e37ed8a24 100644 --- a/kaithem/src/directories.py +++ b/kaithem/src/directories.py @@ -7,14 +7,14 @@ # Log is the log files # []Put these in approprite places when running on linux +import getpass import os import pwd -import getpass import shutil -from .config import config +import socket from os import environ -import socket +from .config import config # Normally we run from one folder. If it's been installed, we change the paths a bit. dn = os.path.dirname(os.path.realpath(__file__)) @@ -29,9 +29,7 @@ def getRootAndroidDir(): Environment = autoclass("android.os.Environment") context = cast("android.content.Context", PythonActivity.mActivity) - user_services_dir = context.getExternalFilesDir( - Environment.getDataDirectory().getAbsolutePath() - ).getAbsolutePath() + user_services_dir = context.getExternalFilesDir(Environment.getDataDirectory().getAbsolutePath()).getAbsolutePath() return os.path.join(user_services_dir, "var") @@ -39,29 +37,13 @@ def getRootAndroidDir(): if "ANDROID_ARGUMENT" in environ: vardir = getRootAndroidDir() datadir = os.path.normpath(os.path.join(dn, "../data")) - logdir = os.path.join( - vardir, "logs", socket.gethostname() + "-" + getpass.getuser() - ) + logdir = os.path.join(vardir, "logs", socket.gethostname() + "-" + getpass.getuser()) else: vardir = os.path.normpath(os.path.join(dn, "..")) vardir = os.path.join(vardir, os.path.expanduser(config["site-data-dir"])) - # These non writable paths indicate we should do stuff differently because we are installed with setuptools - # most likely - if vardir.startswith(("/usr", "/nix")): - vardir = os.path.expanduser("~/kaithem") - - # Override the default for snaps - if vardir.startswith("/snap/"): - if not config["site-data-dir"].startswith("~") or config[ - "site-data-dir" - ].startswith("/"): - vardir = os.path.expanduser("~/" + config["site-data-dir"]) - datadir = os.path.normpath(os.path.join(dn, "../data")) - logdir = os.path.join( - vardir, "logs", socket.gethostname() + "-" + getpass.getuser() - ) + logdir = os.path.join(vardir, "logs", socket.gethostname() + "-" + getpass.getuser()) usersdir = os.path.join(vardir, "users") @@ -90,9 +72,7 @@ def recreate(): vardir = os.path.join(vd, os.path.expanduser(config["site-data-dir"])) usersdir = os.path.join(vardir, "users") - logdir = os.path.join( - vardir, "logs", socket.gethostname() + "-" + getpass.getuser() - ) + logdir = os.path.join(vardir, "logs", socket.gethostname() + "-" + getpass.getuser()) moduledir = os.path.join(vardir, "modules") datadir = os.path.normpath(os.path.join(dn, "../data")) htmldir = os.path.join(dn, "html") diff --git a/kaithem/src/docs/changes.md b/kaithem/src/docs/changes.md index bc225236c..75700d40a 100644 --- a/kaithem/src/docs/changes.md +++ b/kaithem/src/docs/changes.md @@ -3,20 +3,37 @@ Change Log ### 0.78.0 -- :bug: Fix unused subdevice nuisance method resolution error -- :coffin: Remove old baresip code -- :coffin: Remove kaithem.midi API -- :coffin: Remove the image map creator util -- :coffin: Remove kaithem.time.accuracy, lantime, hour, month, day, second, minute, dayofweek -- :coffin: Remove kaithem.sys.shellex, shellexbg, which - -- :coffin: Remove kaithem.events.when and kaithem.events.after +I was not going to release this so early. There may be bugs. YMMV. +However I discovered that the old installer was unreliably due to +some kind of virtualenv behavior where it decides to randomly use +/venv/local/bin instead of /venv/bin. -- :sparkles: Split off the sound stuff in a separate libary [IceMedia](https://github.com/EternityForest/icemedia) meant for easy standalone use. +To fix this, we are moving to pipx and Poetry. To do so I had to get rid of --system-site-packages +completely. This change broke gstreamer, but there is a fix! -- :sparkles: Split off internal scheduling and state machines to [Scullery](https://github.com/EternityForest/scullery) meant for easy standalone use. +Thanks :heart: to [happyleavesaoc](https://github.com/happyleavesaoc/gstreamer-player/) for discovering +a way to make gstreamer work in a virtualenv. All you need to do is symlink the gi package +into your site-packages! Kaithem now does this automatically on Debian and probably most everything +else. -- :sparkles: Add badges(CC0 licensed) +- :bug: Fix unused subdevice nuisance method resolution error +- :coffin::boom: Remove old baresip code +- :coffin::boom: Remove kaithem.midi API +- :coffin::boom: Remove the image map creator util +- :coffin::boom: Remove kaithem.time.accuracy, lantime, hour, month, day, second, minute, dayofweek +- :coffin::boom: Remove kaithem.sys.shellex, shellexbg, which +- :coffin::boom: Remove kaithem.events.when and kaithem.events.after +- :coffin: Remove nixos config that was probably outdated. It's in the log if ya need it! + +- :hammer: Split off the sound stuff in a separate libary [IceMedia](https://github.com/EternityForest/icemedia) meant for easy standalone use. + +- :hammer: Split off internal scheduling and state machines to [Scullery](https://github.com/EternityForest/scullery) meant for easy standalone use. +- :hammer: Move to pyproject.toml +- :hammer: Move to a pipx based install process +- :recycle: A very large amount of old code is gone +- :recycle: Start moving to a proper dialog generation class instead of ad-hoc templates.s +- :lipstick: Add badges(CC0 licensed) +- :lipstick: Slim down the readme ### 0.77.0 diff --git a/kaithem/src/docs/devices.md b/kaithem/src/docs/devices.md index 6b2879c1d..802d954b5 100644 --- a/kaithem/src/docs/devices.md +++ b/kaithem/src/docs/devices.md @@ -23,9 +23,9 @@ resource metadata. ## API -All devices appear in the kaithem.devices[] space. +All devices appear in the kaithem.devices[] space. -All Tagpoints the device exposes appear in the tags list under /devices//. +All Tagpoints the device exposes appear in the tags list under /devices//. ## Dependency Resolution @@ -41,7 +41,7 @@ kaithem.devices is iterable, but does not include anything currently unsupported ### Device Objects -#### dev.alerts(DEPRECATED, USE TAGPOINTS and setAlarm) +#### dev.alerts(DEPRECATED, USE TAGPOINTS and set_alarm) Dict of Alert objects the device defines diff --git a/kaithem/src/html/devices/device.j2.html b/kaithem/src/html/devices/device.j2.html index b17e1b57c..faf70cda4 100644 --- a/kaithem/src/html/devices/device.j2.html +++ b/kaithem/src/html/devices/device.j2.html @@ -333,7 +333,7 @@

Permissions

{{ i| escape }} - {% if hasattr(obj,'_noSetAlarmPriority') %} + {% if hasattr(obj,'_noset_alarmPriority') %} {{ obj.alerts[i].priority }} {% else %}
-
- - - -<%include file="/pagefooter.html"/> diff --git a/kaithem/src/html/modules/events/new.html b/kaithem/src/html/modules/events/new.html deleted file mode 100644 index c74983186..000000000 --- a/kaithem/src/html/modules/events/new.html +++ /dev/null @@ -1,16 +0,0 @@ -<%! -from kaithem.src.util import url -%> -<%include file="/pageheader.html"/> -

Add Event

-Add Event -
-

The new event will be empty and immediately loaded.

-
-
- - -
-
- -<%include file="/pagefooter.html"/> diff --git a/kaithem/src/html/modules/events/run.html b/kaithem/src/html/modules/events/run.html deleted file mode 100644 index 7156ebd2a..000000000 --- a/kaithem/src/html/modules/events/run.html +++ /dev/null @@ -1,16 +0,0 @@ -<%! -from kaithem.src.util import url -%> -<%include file="/pageheader.html"/> -

Manually Run Event

-Manually Run Event -
-

Enter name of event in this module you want to run.

-
-
-

Name


-
-
-
- -<%include file="/pagefooter.html"/> diff --git a/kaithem/src/html/modules/pages/new.html b/kaithem/src/html/modules/pages/new.html deleted file mode 100644 index dbb3105bd..000000000 --- a/kaithem/src/html/modules/pages/new.html +++ /dev/null @@ -1,25 +0,0 @@ -<%! -from kaithem.src.util import url -%> -<%include file="/pageheader.html"/> -

Add Page

-Add Page -
-

The new page will be created basded on a template. Use freeboard for easy no-code dashboards.

-
-
-

Name


- - -
-
-
- -<%include file="/pagefooter.html"/> diff --git a/kaithem/src/kaithemobj.py b/kaithem/src/kaithemobj.py index 7241e9b7c..5a32bf178 100644 --- a/kaithem/src/kaithemobj.py +++ b/kaithem/src/kaithemobj.py @@ -3,42 +3,46 @@ """This is the global general purpose utility thing that is accesable from almost anywhere in user code.""" -import traceback -from . import tagpoints, geolocation -import time +import importlib +import json +import os import random import subprocess import threading -import json -import yaml -import os +import time +import traceback import weakref -from scullery import persist as sculleryPersist - -from typing import Any, Callable, Optional, Dict, List +from typing import Any, Callable, Dict, List, Optional import cherrypy -from . import unitsofmeasure -from . import workers +import jinja2 +import yaml from icemedia import sound_player as sound -from . import messagebus -from . import util -from . import widgets -from . import directories -from . import pages -from . import config -from . import persist -from . import breakpoint +from scullery import persist as sculleryPersist from scullery import statemachines -from . import devices -from . import alerts -from . import theming -from . import assetlib + from kaithem import __version__ +from . import ( + alerts, + assetlib, + breakpoint, + config, + devices, + directories, + geolocation, + messagebus, + pages, + persist, + scriptbindings, + tagpoints, + theming, + unitsofmeasure, + util, + widgets, + workers, +) from . import astrallibwrapper as sky -from . import scriptbindings - wsgi_apps = [] tornado_apps = [] @@ -46,6 +50,21 @@ bootTime = time.time() + +# This is for plugins to use and extend pageheader. +_jl = jinja2.FileSystemLoader( + [os.path.join(directories.htmldir, "jinjatemplates"), "/"], + encoding="utf-8", + followlinks=False, +) + +env = jinja2.Environment(loader=_jl, autoescape=False) + + +def render_jinja_template(template_filename: str, **kw): + return _jl.load(env, template_filename, env.globals).render(imp0rt=importlib.import_module, **kw) + + # Persist is one of the ones that we want to be usable outside of kaithem, so we add our path resolution stuff here. @@ -61,9 +80,7 @@ def resolve_path(fn: str, expand: bool = False): # This exception is what we raise from within the page handler to serve a static file -ServeFileInsteadOfRenderingPageException = ( - pages.ServeFileInsteadOfRenderingPageException -) +ServeFileInsteadOfRenderingPageException = pages.ServeFileInsteadOfRenderingPageException plugins = weakref.WeakValueDictionary() @@ -246,18 +263,14 @@ def strftime(*args): return unitsofmeasure.strftime(*args) @staticmethod - def sunset_time( - lat: Optional[float] = None, lon: Optional[float] = None, date=None - ): + def sunset_time(lat: Optional[float] = None, lon: Optional[float] = None, date=None): if lon is None: lat, lon = geolocation.getCoords() else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.sunset(lat, lon, date) @@ -269,9 +282,7 @@ def sunrise_time(lat=None, lon=None, date=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.sunrise(lat, lon, date) @@ -283,9 +294,7 @@ def civil_dusk_time(lat=None, lon=None, date=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.dusk(lat, lon, date) @@ -297,9 +306,7 @@ def civil_dawn_time(lat=None, lon=None, date=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.dawn(lat, lon, date) @@ -311,9 +318,7 @@ def rahu_start(lat=None, lon=None, date=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.rahu(lat, lon, date)[0] @@ -325,9 +330,7 @@ def rahu_end(lat=None, lon=None, date=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.rahu(lat, lon, date)[1] @@ -339,9 +342,7 @@ def is_dark(lat=None, lon=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.is_dark(lat, lon) @@ -354,9 +355,7 @@ def is_rahu(lat=None, lon=None): else: raise ValueError("You set lon, but not lst?") if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.isRahu(lat, lon) @@ -367,9 +366,7 @@ def is_day(lat=None, lon=None): lat, lon = geolocation.getCoords() if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.is_day(lat, lon) @staticmethod @@ -379,9 +376,7 @@ def is_night(lat=None, lon=None): lat, lon = geolocation.getCoords() if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.is_night(lat, lon) @staticmethod @@ -391,9 +386,7 @@ def is_light(lat=None, lon=None): lat, lon = geolocation.getCoords() if lat is None or lon is None: - raise RuntimeError( - "No server location set, fix this in system settings" - ) + raise RuntimeError("No server location set, fix this in system settings") return sky.is_light(lat, lon) @staticmethod @@ -444,15 +437,13 @@ def add_tornado_app(pattern: str, app, args, permission="system_admin"): def freeboard(page, kwargs, plugins=[]): "Returns the ready-to-embed code for freeboard. Used to unclutter user created pages that use it." if cherrypy.request.method == "POST": - import re import html + import re pages.require("system_admin") c = re.sub( r"<\s*freeboard-data\s*>[\s\S]*<\s*\/freeboard-data\s*>", - "\n" - + html.escape(yaml.dump(json.loads(kwargs["bd"]))) - + "\n", + "\n" + html.escape(yaml.dump(json.loads(kwargs["bd"]))) + "\n", page.getContent(), ) page.setContent(c) @@ -498,10 +489,7 @@ def outputs(): # Always try: - x = [ - i.name - for i in jackmanager.get_ports(is_audio=True, is_input=True) - ] + x = [i.name for i in jackmanager.get_ports(is_audio=True, is_input=True)] except Exception: print(traceback.format_exc()) x = [] @@ -621,9 +609,7 @@ class persist: unsaved = sculleryPersist.unsavedFiles @staticmethod - def load( - fn: str, *args: tuple[Any], **kwargs: Dict[str, Any] - ) -> bytes | str | Dict[Any, Any] | List[Any]: + def load(fn: str, *args: tuple[Any], **kwargs: Dict[str, Any]) -> bytes | str | Dict[Any, Any] | List[Any]: return persist.load(fn, *args, **kwargs) @staticmethod diff --git a/kaithem/src/modules_interface.py b/kaithem/src/modules_interface.py index 856dade79..5867d119c 100644 --- a/kaithem/src/modules_interface.py +++ b/kaithem/src/modules_interface.py @@ -2,37 +2,36 @@ # SPDX-License-Identifier: GPL-3.0-only -import time -import os -import json -import traceback import copy +import json +import logging import mimetypes -import cherrypy +import os +import time +import traceback import weakref -from .util import url + +import cherrypy +from cherrypy.lib.static import serve_file from scullery import scheduling from . import ( auth, - pages, + dialogs, directories, - util, - newevt, - usrpages, messagebus, - schemas, - unitsofmeasure, modules, modules_state, - dialogs, + newevt, + pages, + schemas, + unitsofmeasure, + usrpages, + util, ) -from .modules import external_module_locations - from .config import config -from cherrypy.lib.static import serve_file - -import logging +from .modules import external_module_locations +from .util import url syslog = logging.getLogger("system") searchable = {"event": ["setup", "trigger", "action"], "page": ["body"]} @@ -45,9 +44,7 @@ def get_time(ev): try: if not newevt.EventReferences[ev].nextruntime: return 0 - return newevt.dt_to_ts( - newevt.EventReferences[ev].nextruntime or 0, newevt.EventReferences[ev].tz - ) + return newevt.dt_to_ts(newevt.EventReferences[ev].nextruntime or 0, newevt.EventReferences[ev].tz) except Exception: return -1 @@ -88,9 +85,7 @@ def in_folder(r, f, n): def get_f_size(name, i): try: - return unitsofmeasure.si_format_number( - os.path.getsize(modules_state.fileResourceAbsPaths[name, i]) - ) + return unitsofmeasure.si_format_number(os.path.getsize(modules_state.fileResourceAbsPaths[name, i])) except Exception: return "Could not get size" @@ -100,12 +95,7 @@ def urlForPath(module, path): "/modules/module/" + url(module) + "/resource/" - + "/".join( - [ - url(i.replace("\\", "\\\\").replace("/", "\\/")) - for i in util.split_escape(path[0], "/", "\\")[:-1] - ] - ) + + "/".join([url(i.replace("\\", "\\\\").replace("/", "\\/")) for i in util.split_escape(path[0], "/", "\\")[:-1]]) ) @@ -262,11 +252,8 @@ def search(self, module, **kwargs): def yamldownload(self, module): pages.require("view_admin_info") if config["downloads-include-md5-in-filename"]: - cherrypy.response.headers["Content-Disposition"] = ( - 'attachment; filename="%s"' - % util.url( - f"{module[:-4]}_{modules_state.getModuleHash(module[:-4])}.zip" - ) + cherrypy.response.headers["Content-Disposition"] = 'attachment; filename="%s"' % util.url( + f"{module[:-4]}_{modules_state.getModuleHash(module[:-4])}.zip" ) cherrypy.response.headers["Content-Type"] = "application/zip" try: @@ -290,23 +277,17 @@ def uploadtarget(self, modulesfile, **kwargs): pages.require("system_admin") pages.postOnly() modules_state.modulesHaveChanged() - for i in modules.load_modules_from_zip( - modulesfile.file, replace="replace" in kwargs - ): + for i in modules.load_modules_from_zip(modulesfile.file, replace="replace" in kwargs): pass - messagebus.post_message( - "/system/modules/uploaded", {"user": pages.getAcessingUser()} - ) + messagebus.post_message("/system/modules/uploaded", {"user": pages.getAcessingUser()}) raise cherrypy.HTTPRedirect("/modules/") @cherrypy.expose def index(self): # Require permissions and render page. A lotta that in this file. pages.require("view_admin_info") - return pages.get_template("modules/index.html").render( - ActiveModules=modules_state.ActiveModules - ) + return pages.get_template("modules/index.html").render(ActiveModules=modules_state.ActiveModules) @cherrypy.expose def library(self): @@ -364,9 +345,7 @@ def newmoduletarget(self, **kwargs): # If there is no module by that name, create a blank template and the scope obj with modules_state.modulesLock: if kwargs["name"] in modules_state.ActiveModules: - return pages.get_template("error.html").render( - info=" A module already exists by that name," - ) + return pages.get_template("error.html").render(info=" A module already exists by that name,") modules.newModule(kwargs["name"], kwargs.get("location", None)) raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(kwargs['name'])}") @@ -415,9 +394,7 @@ def module(self, module, *path, **kwargs): if path[0] == "scanfiles": pages.require("system_admin") pages.postOnly() - modules.autoGenerateFileRefResources( - modules_state.ActiveModules[root], root - ) + modules.autoGenerateFileRefResources(modules_state.ActiveModules[root], root) raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(root)}") if path[0] == "runevent": @@ -430,9 +407,11 @@ def module(self, module, *path, **kwargs): # There might be a password or something important in the actual module object. Best to restrict who can access it. pages.require("system_admin") cherrypy.response.headers["X-Frame-Options"] = "SAMEORIGIN" - return pages.get_template("modules/events/run.html").render( - module=root, event=path[1] - ) + + d = dialogs.Dialog("Run event manually") + d.text_input("name", default=path[1]) + d.submit_button("Run") + return d.render(f"/modules/module/{url(module)}/runevent") if path[0] == "obj": # There might be a password or something important in the actual module object. Best to restrict who can access it. @@ -464,9 +443,7 @@ def module(self, module, *path, **kwargs): objname = kwargs["objname"] if "objpath" not in kwargs: - return pages.get_template("modules/modulescope.html").render( - kwargs=kwargs, name=root, obj=obj, objname=objname - ) + return pages.get_template("modules/modulescope.html").render(kwargs=kwargs, name=root, obj=obj, objname=objname) else: return pages.get_template("obj_insp.html").render( objpath=kwargs["objpath"], @@ -523,8 +500,7 @@ def module(self, module, *path, **kwargs): if os.path.isfile(dataname): return serve_file( dataname, - content_type=mimetypes.guess_type(path[1], False)[0] - or "application/x-unknown", + content_type=mimetypes.guess_type(path[1], False)[0] or "application/x-unknown", disposition="inline;", name=path[1], ) @@ -538,9 +514,7 @@ def module(self, module, *path, **kwargs): else: x = "" # path[1] tells what type of resource is being created and addResourceDispatcher returns the appropriate crud screen - return pages.get_template("modules/uploadfileresource.html").render( - module=module, path=x - ) + return pages.get_template("modules/uploadfileresource.html").render(module=module, path=x) # This goes to a dispatcher that takes into account the type of resource and updates everything about the resource. if path[0] == "uploadfileresourcetarget": @@ -573,9 +547,7 @@ def module(self, module, *path, **kwargs): with modules_state.modulesLock: # BEGIN BLOCK OF CODE COPY PASTED FROM ANOTHER PART OF CODE. I DO NOT REALLY UNDERSTAND IT # Wow is this code ever ugly. Bascially we are going to pack the path and the module together. - escapedName = ( - kwargs["name"].replace("\\", "\\\\").replace("/", "\\/") - ) + escapedName = kwargs["name"].replace("\\", "\\\\").replace("/", "\\/") if len(path) > 1: escapedName = f"{path[1]}/{escapedName}" x = util.split_escape(module, "/", "\\") @@ -592,8 +564,7 @@ def insertResource(r): d = { "resource-type": "internal-fileref", "serve": "serve" in kwargs, - "target": "$MODULERESOURCES/" - + util.url(escapedName, modules.safeFnChars), + "target": "$MODULERESOURCES/" + util.url(escapedName, modules.safeFnChars), } # Preserve existing metadata @@ -606,9 +577,7 @@ def insertResource(r): modules.handleResourceChange(root, escapedName) modules_state.modulesHaveChanged() if len(path) > 1: - raise cherrypy.HTTPRedirect( - f"/modules/module/{util.url(root)}/resource/{util.url(path[1])}" - ) + raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(root)}/resource/{util.url(path[1])}") else: raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(root)}") @@ -634,12 +603,7 @@ def insertResource(r): messagebus.post_message( "/system/notifications", - "User " - + pages.getAcessingUser() - + " deleted resource " - + kwargs["name"] - + " from module " - + module, + "User " + pages.getAcessingUser() + " deleted resource " + kwargs["name"] + " from module " + module, ) messagebus.post_message( "/system/modules/deletedresource", @@ -652,10 +616,7 @@ def insertResource(r): ) if len(util.split_escape(kwargs["name"], "/", "\\")) > 1: raise cherrypy.HTTPRedirect( - "/modules/module/" - + util.url(module) - + "/resource/" - + util.url(util.module_onelevelup(kwargs["name"])) + "/modules/module/" + util.url(module) + "/resource/" + util.url(util.module_onelevelup(kwargs["name"])) ) else: raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(module)}") @@ -669,10 +630,7 @@ def insertResource(r): if "location" in kwargs and kwargs["location"]: external_module_locations[kwargs["name"]] = kwargs["location"] # We can't just do a delete and then set, what if something odd happens between? - if ( - not kwargs["name"] == root - and root in external_module_locations - ): + if not kwargs["name"] == root and root in external_module_locations: del external_module_locations[root] else: # We must delete this before deleting the actual external_module_locations entry @@ -697,9 +655,7 @@ def insertResource(r): external_module_locations.pop(root) # Missing descriptions have caused a lot of bugs if "__description" in modules_state.ActiveModules[root]: - modules_state.ActiveModules[root]["__description"]["text"] = ( - kwargs["description"] - ) + modules_state.ActiveModules[root]["__description"]["text"] = kwargs["description"] else: modules_state.ActiveModules[root]["__description"] = { "resource-type": "module-description", @@ -709,9 +665,7 @@ def insertResource(r): # Renaming reloads the entire module. # TODO This needs to handle custom resource types if we ever implement them. if not kwargs["name"] == root: - modules_state.ActiveModules[kwargs["name"]] = ( - modules_state.ActiveModules.pop(root) - ) + modules_state.ActiveModules[kwargs["name"]] = modules_state.ActiveModules.pop(root) # UHHG. So very much code tht just syncs data structures. # This gets rid of the cache under the old name newevt.removeModuleEvents(root) @@ -720,9 +674,7 @@ def insertResource(r): modules.bookkeeponemodule(kwargs["name"], update=True) # Just for fun, we should probably also sync the permissions auth.importPermissionsFromModules() - raise cherrypy.HTTPRedirect( - f"/modules/module/{util.url(kwargs['name'])}" - ) + raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(kwargs['name'])}") # Return a CRUD screen to create a new resource taking into the type of resource the user wants to create @@ -732,32 +684,18 @@ def addResourceDispatcher(module, type, path): pages.require("system_admin") # Return a crud to add a new permission - if type == "permission": - d = dialogs.Dialog("New Permission in {module}") + if type in ("permission", "event", "page", "directory"): + d = dialogs.Dialog(f"New {type.capitalize()} in {module}") d.text_input("name") - d.text_input("description") - d.submit_button("Submit") - return d.render( - f"/modules/module/{url(module)}/addresourcetarget/permission/{url(path)}" - ) - # return a crud to add a new event - elif type == "event": - return pages.get_template("modules/events/new.html").render( - module=module, path=path - ) + if type in ("permission",): + d.text_input("description") - # return a crud to add a new event - elif type == "page": - return pages.get_template("modules/pages/new.html").render( - module=module, path=path - ) + if type == "page": + d.selection("template", options=["default", "freeboard"]) - # return a crud to add a new event - elif type == "directory": - return pages.get_template("modules/directories/new.html").render( - module=module, path=path - ) + d.submit_button("Create") + return d.render(f"/modules/module/{url(module)}/addresourcetarget/{type}/{url(path)}") else: return modules_state.additionalTypes[type].createpage(module, path) @@ -794,9 +732,7 @@ def insertResource(r): raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(module)}") elif type == "permission": - insertResource( - {"resource-type": "permission", "description": kwargs["description"]} - ) + insertResource({"resource-type": "permission", "description": kwargs["description"]}) # has its own lock auth.importPermissionsFromModules() # sync auth's list of permissions @@ -834,19 +770,10 @@ def insertResource(r): messagebus.post_message( "/system/notifications", - "User " - + pages.getAcessingUser() - + " added resource " - + escapedName - + " of type " - + type - + " to module " - + root, + "User " + pages.getAcessingUser() + " added resource " + escapedName + " of type " + type + " to module " + root, ) # Take the user straight to the resource page - raise cherrypy.HTTPRedirect( - f"/modules/module/{util.url(module)}/resource/{util.url(escapedName)}" - ) + raise cherrypy.HTTPRedirect(f"/modules/module/{util.url(module)}/resource/{util.url(escapedName)}") # show a edit page for a resource. No side effect here so it only requires the view permission @@ -861,9 +788,7 @@ def resourceEditPage(module, resource, version="default", kwargs={}): elif version == "__default__": try: - resourceinquestion = modules_state.ActiveModules[module][resource][ - "versions" - ]["__draft__"] + resourceinquestion = modules_state.ActiveModules[module][resource]["versions"]["__draft__"] version = "__draft__" except KeyError: version = "__live__" @@ -921,9 +846,7 @@ def resourceEditPage(module, resource, version="default", kwargs={}): ) # This is for the custom resource types interface stuff. - return modules_state.additionalTypes[ - resourceinquestion["resource-type"] - ].editpage(module, resource, resourceinquestion) + return modules_state.additionalTypes[resourceinquestion["resource-type"]].editpage(module, resource, resourceinquestion) def permissionEditPage(module, resource): @@ -968,15 +891,14 @@ def resourceUpdateTarget(module, resource, kwargs): resourceobj["serve"] = "serve" in kwargs # has its own lock resourceobj["allow-xss"] = "allow-xss" in kwargs - resourceobj["allow-origins"] = [ - i.strip() for i in kwargs["allow-origins"].split(",") - ] + resourceobj["allow-origins"] = [i.strip() for i in kwargs["allow-origins"].split(",")] resourceobj["mimetype"] = kwargs["mimetype"] # Just like pages, file resources are permissioned resourceobj["require-permissions"] = [] for i in kwargs: - # Since HTTP args don't have namespaces we prefix all the permission checkboxes with permission + # Since HTTP args don't have namespaces we prefix all the + # permission checkboxes with permission if i[:10] == "Permission": if kwargs[i] == "true": resourceobj["require-permissions"].append(i[10:]) @@ -1009,15 +931,14 @@ def resourceUpdateTarget(module, resource, kwargs): # Test for syntax errors at least, before we do anything more newevt.test_compile(setupcode, actioncode) - # Remove the old event even before we even do a test run of setup. If we can't do the new version just put the old one back. + # Remove the old event even before we even do a test run of setup. + # If we can't do the new version just put the old one back. # Todo actually put old one back newevt.removeOneEvent(module, resource) # Leave a delay so that effects of cleanup can fully propagate. time.sleep(0.08) # Make event from resource, but use our substitute modified dict - compiled_object = newevt.make_event_from_resource( - module, resource, r2 - ) + compiled_object = newevt.make_event_from_resource(module, resource, r2) except Exception: if "versions" not in resourceobj: @@ -1049,7 +970,8 @@ def resourceUpdateTarget(module, resource, kwargs): r2["rate-limit"] = float(kwargs["ratelimit"]) r2["enable"] = False - # Remove the old event even before we do a test compile. If we can't do the new version just put the old one back. + # Remove the old event even before we do a test compile. + # If we can't do the new version just put the old one back. newevt.removeOneEvent(module, resource) # Leave a delay so that effects of cleanup can fully propagate. time.sleep(0.08) @@ -1058,7 +980,8 @@ def resourceUpdateTarget(module, resource, kwargs): modules_state.ActiveModules[module][resource] = r2 # I really need to do something about this possibly brittle bookkeeping system - # But anyway, when the active modules thing changes we must update the newevt cache thing. + # But anyway, when the active modules thing changes we must update + # the newevt cache thing. # Delete the draft if any try: @@ -1099,9 +1022,7 @@ def resourceUpdateTarget(module, resource, kwargs): resourceobj["no-header"] = "no-header" in kwargs resourceobj["auto-reload"] = "autoreload" in kwargs resourceobj["allow-xss"] = "allow-xss" in kwargs - resourceobj["allow-origins"] = [ - i.strip() for i in kwargs["allow-origins"].split(",") - ] + resourceobj["allow-origins"] = [i.strip() for i in kwargs["allow-origins"].split(",")] resourceobj["auto-reload-interval"] = float(kwargs["autoreloadinterval"]) # Method checkboxes resourceobj["require-method"] = [] @@ -1112,7 +1033,8 @@ def resourceUpdateTarget(module, resource, kwargs): # permission checkboxes resourceobj["require-permissions"] = [] for i in kwargs: - # Since HTTP args don't have namespaces we prefix all the permission checkboxes with permission + # Since HTTP args don't have namespaces we prefix all the permission + # checkboxes with permission if i[:10] == "Permission": if kwargs[i] == "true": resourceobj["require-permissions"].append(i[10:]) @@ -1128,36 +1050,27 @@ def resourceUpdateTarget(module, resource, kwargs): # Just handles the internal stuff modules.mvResource(module, resource, module, kwargs["name"]) - # We can pass a compiled object for things like events that would otherwise have to have a test compile then the real compile - modules.handleResourceChange( - module, kwargs.get("name", resource), compiled_object - ) + # We can pass a compiled object for things like events that would otherwise + # have to have a test compile then the real compile + modules.handleResourceChange(module, kwargs.get("name", resource), compiled_object) prev_versions[(module, resource)] = old_resource messagebus.post_message( "/system/notifications", - "User " - + pages.getAcessingUser() - + " modified resource " - + resource - + " of module " - + module, + "User " + pages.getAcessingUser() + " modified resource " + resource + " of module " + module, ) r = resource if "name" in kwargs: r = kwargs["name"] if "GoNow" in kwargs: raise cherrypy.HTTPRedirect(usrpages.url_for_resource(module, r)) - # Return user to the module page. If name has a folder, return the user to it;s containing folder. + # Return user to the module page. If name has a folder, return the + # user to it;s containing folder. x = util.split_escape(r, "/", "\\") if len(x) > 1: raise cherrypy.HTTPRedirect( - "/modules/module/" - + util.url(module) - + "/resource/" - + "/".join([util.url(i) for i in x[:-1]]) - + "#resources" + "/modules/module/" + util.url(module) + "/resource/" + "/".join([util.url(i) for i in x[:-1]]) + "#resources" ) else: # +'/resource/'+util.url(resource)) diff --git a/kaithem/src/pages.py b/kaithem/src/pages.py index e8a01802f..256926f4a 100644 --- a/kaithem/src/pages.py +++ b/kaithem/src/pages.py @@ -1,18 +1,19 @@ # SPDX-FileCopyrightText: Copyright 2013 Daniel Dunn # SPDX-License-Identifier: GPL-3.0-only -import importlib -from mako.lookup import TemplateLookup -import cherrypy import base64 -import weakref -import time +import importlib import logging -import os import mimetypes +import os +import time +import weakref + +import cherrypy import jinja2 -from . import auth -from . import directories +from mako.lookup import TemplateLookup + +from . import auth, directories _Lookup = TemplateLookup( directories=[ @@ -27,7 +28,7 @@ # _jl = jinja2.FileSystemLoader( - [directories.htmldir, os.path.join(directories.htmldir, "jinjatemplates")], + [directories.htmldir, os.path.join(directories.htmldir, "jinjatemplates"), "/"], encoding="utf-8", followlinks=False, ) @@ -62,12 +63,7 @@ def get_vardir_template(fn): # There are cases where this may not exactly be perfect, # but the point is just an extra guard against user error. def isHTTPAllowed(ip): - return ( - ip.startswith( - ("::1", "127.", "::ffff:192.", "::ffff:10.", "192.", "10.", "fc", "fd") - ) - or ip == "::ffff:127.0.0.1" - ) + return ip.startswith(("::1", "127.", "::ffff:192.", "::ffff:10.", "192.", "10.", "fc", "fd")) or ip == "::ffff:127.0.0.1" nativeHandlers = weakref.WeakValueDictionary() @@ -117,9 +113,7 @@ def require(permission, noautoreturn=False): """ if permission == "__never__": - raise RuntimeError( - "Nobody has the __never__ permission, ever, except in nosecurity mode." - ) + raise RuntimeError("Nobody has the __never__ permission, ever, except in nosecurity mode.") if not isinstance(permission, str): p = permission @@ -135,9 +129,7 @@ def require(permission, noautoreturn=False): if user == "__no_request__": return - if permission in auth.crossSiteRestrictedPermissions or not auth.getUserSetting( - user, "allow-cors" - ): + if permission in auth.crossSiteRestrictedPermissions or not auth.getUserSetting(user, "allow-cors"): noCrossSite() # If the special __guest__ user can do it, anybody can. @@ -170,12 +162,7 @@ def require(permission, noautoreturn=False): # than that and it takes him back to the main page. # This is so it can't auto redirect # To something you forgot about and no longer want. - raise cherrypy.HTTPRedirect( - "/login?go=" - + base64.b64encode(url.encode()).decode() - + "&maxgotime-" - + str(time.time() + 300) - ) + raise cherrypy.HTTPRedirect("/login?go=" + base64.b64encode(url.encode()).decode() + "&maxgotime-" + str(time.time() + 300)) if not auth.canUserDoThis(user, permission): raise cherrypy.HTTPRedirect("/errors/permissionerror?") @@ -206,9 +193,7 @@ def noCrossSite(): def strictNoCrossSite(): if not cherrypy.request.base == cherrypy.request.headers.get("Origin", ""): - raise PermissionError( - "Cannot make this request from a different origin, or from a requester that does not provide an origin" - ) + raise PermissionError("Cannot make this request from a different origin, or from a requester that does not provide an origin") def getAcessingUser(tornado_mode=None): @@ -226,11 +211,7 @@ def getAcessingUser(tornado_mode=None): base = tornado_mode.host else: - if ( - (not cherrypy.request.request_line) - and (not cherrypy.request.app) - and (not cherrypy.request.config) - ): + if (not cherrypy.request.request_line) and (not cherrypy.request.app) and (not cherrypy.request.config): return "__no_request__" headers = cherrypy.request.headers scheme = cherrypy.request.scheme @@ -268,13 +249,7 @@ def getAcessingUser(tornado_mode=None): user = auth.whoHasToken(cookie["kaithem_auth"].value) if not auth.getUserSetting(user, "allow-cors"): if headers.get("Origin", ""): - x = ( - headers.get("Origin", "") - .replace("http://", "") - .replace("https://", "") - .replace("ws://", "") - .replace("wss://", "") - ) + x = headers.get("Origin", "").replace("http://", "").replace("https://", "").replace("ws://", "").replace("wss://", "") x2 = headers.get("Origin", "") # Cherrypy and tornado compatibility if base not in (x, x2): diff --git a/kaithem/src/pathsetup.py b/kaithem/src/pathsetup.py index 6012a9cc2..c97a0bc11 100644 --- a/kaithem/src/pathsetup.py +++ b/kaithem/src/pathsetup.py @@ -4,9 +4,9 @@ # This file deals with configuring the way python's import mechanism works. -import sys -import os import logging +import os +import sys logger = logging.getLogger("system") @@ -45,6 +45,9 @@ def setupPath(linuxpackage=None, force_local=False): # Low priority modules will default to using the version installed on the user's computer. sys.path = sys.path + [os.path.join(x, "thirdparty", "lowpriority")] + # Truly an awefullehaccken + # Break out of venv to get to gstreamer + # Consider using importlib.util.module_for_loader() to handle # most of these details for you. @@ -53,5 +56,27 @@ def load_module(self, fullname): if fullname.endswith(i): return sys.modules[i] + # Truly an awefullehaccken + # Break out of venv to get to gstreamer + # It's just that one package. Literally everything else + # Is perfectly fine. GStreamer doesn't do pip so we do this. + + try: + if os.environ.get("VIRTUAL_ENV"): + en = os.environ["VIRTUAL_ENV"] + p = os.path.join( + en, + "lib", + "python" + ".".join(sys.version.split(".")[:2]), + "site-packages", + "gi", + ) + s = "/usr/lib/python3/dist-packages/gi" + + if os.path.exists(s) and (not os.path.exists(p)): + os.symlink(s, p) + except Exception: + logger.exception("Failed to do the gstreamer hack") + setupPath() diff --git a/kaithem/src/plugins/startup/SystemStatusPlugin/__init__.py b/kaithem/src/plugins/startup/SystemStatusPlugin/__init__.py index 4e0ec4284..aab17fd07 100644 --- a/kaithem/src/plugins/startup/SystemStatusPlugin/__init__.py +++ b/kaithem/src/plugins/startup/SystemStatusPlugin/__init__.py @@ -1,9 +1,11 @@ -import threading -from scullery import scheduling -from kaithem.src import util, alerts, tagpoints, messagebus -import subprocess import logging import os +import subprocess +import threading + +from scullery import scheduling + +from kaithem.src import alerts, messagebus, tagpoints, util undervoltageDuringBootPosted = False overTempDuringBootPosted = False @@ -11,8 +13,8 @@ def getSDHealth(): - import os import json + import os p = None if os.path.exists("/dev/shm/sdmon_cache_mmcblk0"): @@ -40,14 +42,9 @@ def getSDHealth(): battery = psutil.sensors_battery() except ImportError: - logging.exception("Cant load psutil, trying plyer instead") + logging.exception("Cant load psutil") psutil = None - try: - import plyer - except ImportError: - print("Plyer not available either") - if battery: batteryTag = tagpoints.Tag("/system/power/battery_level") batteryTag.value = battery.percent @@ -61,9 +58,7 @@ def getSDHealth(): battery_time.max = 30 * 60 * 60 battery_time.lo = 40 * 60 battery_time.value = battery.secsleft if battery.secsleft > 0 else 9999999 - battery_time.set_alarm( - "lowbattery_timeRemaining", "value < 60*15", priority="error" - ) + battery_time.set_alarm("lowbattery_timeRemaining", "value < 60*15", priority="error") acPowerTag = tagpoints.Tag("/system/power/charging") acPowerTag.value = battery.power_plugged or 0 @@ -144,7 +139,6 @@ def doDiskSpaceCheck(): @scheduling.scheduler.every_minute def doPsutil(): - temps = {} t = psutil.sensors_temperatures() for i in t: peak = 0 @@ -160,16 +154,14 @@ def doPsutil(): if i not in tempTags: # Fix the name - tempTags[i] = tagpoints.Tag( - tagpoints.normalize_tag_name("/system/sensors/temp/" + i, "_") - ) - tempTags[i].setAlarm( + tempTags[i] = tagpoints.Tag(tagpoints.normalize_tag_name("/system/sensors/temp/" + i, "_")) + tempTags[i].set_alarm( "temperature", "value>78", releaseCondition="value<65", priority="warning", ) - tempTags[i].setAlarm("lowtemperature", "value<5") + tempTags[i].set_alarm("lowtemperature", "value<5") tempTags[i].unit = "degC" tempTags[i].max = 150 diff --git a/kaithem/src/tagpoints.py b/kaithem/src/tagpoints.py index 9858b1798..95578b169 100644 --- a/kaithem/src/tagpoints.py +++ b/kaithem/src/tagpoints.py @@ -2,48 +2,47 @@ # SPDX-License-Identifier: GPL-3.0-only from __future__ import annotations -import typing -from typing import final -from . import widgets -from .unitsofmeasure import convert, unit_types -from scullery import scheduling -from . import ( - workers, - messagebus, - directories, - persist, - alerts, - taghistorian, - util, -) -import time -import threading -import weakref +import copy +import functools +import gc +import json import logging -import types -import traceback import math import os -import gc -import functools -import re import random -import json -import copy - -import dateutil -import dateutil.parser - +import re +import threading +import time +import traceback +import types +import typing +import weakref +from collections.abc import Callable from typing import ( Any, - Optional, - TypeVar, Generic, + TypeVar, + final, ) -from collections.abc import Callable + +import dateutil +import dateutil.parser +from scullery import scheduling from typeguard import typechecked +from . import ( + alerts, + directories, + messagebus, + persist, + taghistorian, + util, + widgets, + workers, +) +from .unitsofmeasure import convert, unit_types + def make_tag_info_helper(t: GenericTagPointClass[Any]): def f(): @@ -59,9 +58,7 @@ def f(): logger = logging.getLogger("tagpoints") syslogger = logging.getLogger("system") -exposedTags: weakref.WeakValueDictionary[str, GenericTagPointClass[Any]] = ( - weakref.WeakValueDictionary() -) +exposedTags: weakref.WeakValueDictionary[str, GenericTagPointClass[Any]] = weakref.WeakValueDictionary() # These are the atrtibutes of a tag that can be overridden by configuration. # Setting tag.hi sets the runtime property, but we ignore it if the configuration takes precedence. @@ -233,9 +230,7 @@ def __init__(self: GenericTagPointClass[T], name: str): global allTagsAtomic _name: str = normalize_tag_name(name) if _name in allTags: - raise RuntimeError( - "Tag with this name already exists, use the getter function to get it instead" - ) + raise RuntimeError("Tag with this name already exists, use the getter function to get it instead") # Todo WHY cant we type it as claim[T]?? self.kweb_manual_override_claim: Claim[Any] | None @@ -545,9 +540,7 @@ def expose( # The tag.control version is exactly the same but output-only, # so you can have a synced UI widget that # can store the UI setpoint state even when the actual tag is overriden. - self.dataSourceAutoControl = widgets.DataSource( - id=f"tag.control:{self.name}" - ) + self.dataSourceAutoControl = widgets.DataSource(id=f"tag.control:{self.name}") self.dataSourceAutoControl.write(None) w.set_permissions( [i.strip() for i in d2[0].split(",")], @@ -680,9 +673,7 @@ def f(): if not self.alreadyPostedDeadlock: messagebus.post_message( "/system/notifications/errors", - "Tag point: " - + self.name - + " has been unavailable for 30s and may be involved in a deadlock. see threads view.", + "Tag point: " + self.name + " has been unavailable for 30s and may be involved in a deadlock. see threads view.", ) self.alreadyPostedDeadlock = True @@ -708,9 +699,7 @@ def _recordConfigAttr(self, k: str, v: Any): if v not in (None, "") and (v.strip() if isinstance(v, str) else True): self.configOverrides[k] = v if self.name not in configTagData: - configTagData[self.name] = persist.getStateFile( - get_filename_for_tag_config(self.name) - ) + configTagData[self.name] = persist.getStateFile(get_filename_for_tag_config(self.name)) configTagData[self.name][k] = v configTagData[self.name].noFileForEmpty = True configTagData[self.name][k] = v @@ -820,9 +809,7 @@ def set_alarm( if isConfigured: if not isinstance(condition, str) and condition is not None: - raise ValueError( - "Configurable alarms only allow str or none condition" - ) + raise ValueError("Configurable alarms only allow str or none condition") hasUnsavedData[0] = True storage = self.configuredAlarmData @@ -831,9 +818,7 @@ def set_alarm( # Dynamics are weak reffed if not _refresh: # This is because we need somewhere to return the strong ref - raise RuntimeError( - "Cannot create dynamic alarm without the refresh option" - ) + raise RuntimeError("Cannot create dynamic alarm without the refresh option") if condition is None: try: @@ -850,9 +835,7 @@ def set_alarm( if isConfigured: if self.configuredAlarmData: if self.name not in configTagData: - configTagData[self.name] = persist.getStateFile( - get_filename_for_tag_config(self.name) - ) + configTagData[self.name] = persist.getStateFile(get_filename_for_tag_config(self.name)) configTagData[self.name].noFileForEmpty = True configTagData[self.name]["alarms"] = self.configuredAlarmData @@ -1019,13 +1002,9 @@ def _alarm_from_data(self, name: str, d: dict): # Shallow copy, because we are going to override the tag getter context = copy.copy(self.evalContext) - tripCondition = compile( - tripCondition, f"{self.name}.alarms.{name}_trip", "eval" - ) + tripCondition = compile(tripCondition, f"{self.name}.alarms.{name}_trip", "eval") if releaseCondition: - releaseCondition = compile( - releaseCondition, f"{self.name}.alarms.{name}_release", "eval" - ) + releaseCondition = compile(releaseCondition, f"{self.name}.alarms.{name}_release", "eval") n = self.name.replace("=", "expr_") for i in ILLEGAL_NAME_CHARS: @@ -1042,9 +1021,7 @@ def _alarm_from_data(self, name: str, d: dict): if t: t.unsubscribe(oldAlert.recalcFunction) except Exception: - logger.exception( - "cleanup err, could be because it was already deleted" - ) + logger.exception("cleanup err, could be because it was already deleted") refs = self._alarmGCRefs.pop(name, None) if refs: @@ -1108,15 +1085,11 @@ def alarmPollFunction(value, timestamp, annotation): obj.notificationHTML = self._makeTagAlarmHTMLFunc(weakref.ref(self)) - generatedRecalcFuncWeMustKeepARefTo = self._getAlarmContextGetters( - obj, context, weakref.ref(alarm_recalc_function) - ) + generatedRecalcFuncWeMustKeepARefTo = self._getAlarmContextGetters(obj, context, weakref.ref(alarm_recalc_function)) self._alarmGCRefs[name] = ( alarm_recalc_function, - scheduling.scheduler.schedule_repeating( - alarm_recalc_function, 60, sync=False - ), + scheduling.scheduler.schedule_repeating(alarm_recalc_function, 60, sync=False), alarmPollFunction, generatedRecalcFuncWeMustKeepARefTo, ) @@ -1158,23 +1131,15 @@ def set_config_data(self, data: dict[str, Any]): self._runtime_config_data.update(data) if data and self.name not in configTagData: - configTagData[self.name] = persist.getStateFile( - get_filename_for_tag_config(self.name) - ) + configTagData[self.name] = persist.getStateFile(get_filename_for_tag_config(self.name)) configTagData[self.name].noFileForEmpty = True if "type" in data: - if data["type"] == "number" and not isinstance( - self, NumericTagPointClass - ): + if data["type"] == "number" and not isinstance(self, NumericTagPointClass): raise RuntimeError("Tag already exists and is not a numeric tag") - if data["type"] == "string" and not isinstance( - self, StringTagPointClass - ): + if data["type"] == "string" and not isinstance(self, StringTagPointClass): raise RuntimeError("Tag already exists and is not a string tag") - if data["type"] == "object" and not isinstance( - self, ObjectTagPointClass - ): + if data["type"] == "object" and not isinstance(self, ObjectTagPointClass): raise RuntimeError("Tag already exists and is not an object tag") # Only modify tags if the current data matches the existing @@ -1212,9 +1177,7 @@ def set_config_data(self, data: dict[str, Any]): interval = float(i.get("interval", 60) or 60) target = i.get("target", "disk") - length = float( - i.get("historyLength", 3 * 30 * 24 * 3600) or 3 * 30 * 24 * 3600 - ) + length = float(i.get("historyLength", 3 * 30 * 24 * 3600) or 3 * 30 * 24 * 3600) accum = i["accumulate"] try: @@ -1226,14 +1189,11 @@ def set_config_data(self, data: dict[str, Any]): except Exception: messagebus.post_message( "/system/notifications/errors", - "Error creating logger for: " - + self.name - + "\n" - + traceback.format_exc(), + "Error creating logger for: " + self.name + "\n" + traceback.format_exc(), ) # this is apparently just for the configured part, the dynamic part happens behind the scenes in - # setAlarm via createAlarma + # set_alarm via createAlarma alarms = data.get("alarms", {}) self.configuredAlarmData = {} for i in alarms: @@ -1253,16 +1213,12 @@ def set_config_data(self, data: dict[str, Any]): if self.timestamp == 0: # Set timestamp to 0, this marks the tag as still using a default # Which can be further changed - self.set_claim_val( - "default", data["value"], 0, "Configured default" - ) + self.set_claim_val("default", data["value"], 0, "Configured default") else: if self.timestamp == 0: # Set timestamp to 0, this marks the tag as still using a default # Which can be further changed - self.set_claim_val( - "default", self.default, 0, "Configured default" - ) + self.set_claim_val("default", self.default, 0, "Configured default") else: if self.name in configTagData: configTagData[self.name].pop("value", 0) @@ -1345,17 +1301,13 @@ def set_config_data(self, data: dict[str, Any]): # Set configured permissions, overriding runtime self.expose(*p, configured=True) - def createGetterFromExpression( - self: GenericTagPointClass[T], e: str, priority=98 - ) -> Claim[T]: + def createGetterFromExpression(self: GenericTagPointClass[T], e: str, priority=98) -> Claim[T]: "Create a getter for tag self using expression e" try: for i in self.source_tags: self.source_tags[i].unsubscribe(self.recalc) except Exception: - logger.exception( - "Unsubscribe fail to old tag. A subscription mau be leaked, wasting CPU. This should not happen." - ) + logger.exception("Unsubscribe fail to old tag. A subscription mau be leaked, wasting CPU. This should not happen.") self.source_tags = {} @@ -1392,9 +1344,7 @@ def interval(self, val): else: self._interval = 0 - messagebus.post_message( - f"/system/tags/interval{self.name}", self._interval, synchronous=True - ) + messagebus.post_message(f"/system/tags/interval{self.name}", self._interval, synchronous=True) with self.lock: self._manage_polling() @@ -1439,10 +1389,7 @@ def Tag(cls, name: str, defaults: dict[str, Any] = {}): if x: if x.__class__ is not cls: raise TypeError( - "A tag of that name exists, but it is the wrong type. Existing: " - + str(x.__class__) - + " New: " - + str(cls) + "A tag of that name exists, but it is the wrong type. Existing: " + str(x.__class__) + " New: " + str(cls) ) rval = x @@ -1512,17 +1459,13 @@ def _manage_polling(self): self.poller.unregister() self.poller = None - self.poller = scheduling.scheduler.schedule_repeating( - self.poll, interval, sync=False - ) + self.poller = scheduling.scheduler.schedule_repeating(self.poll, interval, sync=False) else: if self.poller: self.poller.unregister() self.poller = None - def fast_push( - self, value: T, timestamp: float | None = None, annotation: Any = None - ) -> None: + def fast_push(self, value: T, timestamp: float | None = None, annotation: Any = None) -> None: """ Push a value to all subscribers. Does not set the tag's value. Ignores any and all overriding claims. @@ -1573,19 +1516,11 @@ def subscribe(self, f: Callable[[T, float, Any], Any], immediate: bool = False): def errcheck(*a: Any): if time.monotonic() < timestamp - 0.5: - logging.warning( - "Function: " - + desc - + " was deleted 0.5s after being subscribed. This is probably not what you wanted." - ) + logging.warning("Function: " + desc + " was deleted 0.5s after being subscribed. This is probably not what you wanted.") if self.lock.acquire(timeout=20): try: - ref: ( - weakref.WeakMethod[Callable[[T, float, Any], Any]] - | weakref.ref[Callable[[T, float, Any], Any]] - | None - ) = None + ref: weakref.WeakMethod[Callable[[T, float, Any], Any]] | weakref.ref[Callable[[T, float, Any], Any]] | None = None if isinstance(f, types.MethodType): ref = weakref.WeakMethod(f, errcheck) @@ -1625,9 +1560,7 @@ def errcheck(*a: Any): self.lock.release() else: self.testForDeadlock() - raise RuntimeError( - "Cannot get lock to subscribe to this tag. Is there a long running subscriber?" - ) + raise RuntimeError("Cannot get lock to subscribe to this tag. Is there a long running subscriber?") @typechecked def unsubscribe(self, f: Callable): @@ -1650,9 +1583,7 @@ def unsubscribe(self, f: Callable): self.lock.release() else: self.testForDeadlock() - raise RuntimeError( - "Cannot get lock to subscribe to this tag. Is there a long running subscriber?" - ) + raise RuntimeError("Cannot get lock to subscribe to this tag. Is there a long running subscriber?") @typechecked def setHandler(self, f: Callable[[T, float, Any], Any]): @@ -1712,20 +1643,13 @@ def _push(self): ) except Exception as e: extraData = str(e) - logger.exception( - f"Tag subscriber error, val,time,annotation was: {extraData}" - ) + logger.exception(f"Tag subscriber error, val,time,annotation was: {extraData}") # Return the error from whence it came to display in the proper place for i in subscriber_error_handlers: try: i(self, f, self.lastValue) except Exception: - print( - "Failed to handle error: " - + traceback.format_exc(6) - + "\nData: " - + extraData - ) + print("Failed to handle error: " + traceback.format_exc(6) + "\nData: " + extraData) del f def processValue(self, value) -> T: @@ -1798,11 +1722,7 @@ def _get_value(self, force=False) -> T: # We extend the idea that cache is allowed to also # mean we can fall back to cache in case of a timeout. else: - logging.error( - "tag point:" - + self.name - + " took too long getting lock to get value, falling back to cache" - ) + logging.error("tag point:" + self.name + " took too long getting lock to get value, falling back to cache") return self.lastValue try: # None means no new data @@ -1833,21 +1753,14 @@ def _get_value(self, force=False) -> T: # The system logger is the one kaithem actually logs to file. if self.lastError < (time.monotonic() - (60 * 10)): self.lastError = time.monotonic() - syslogger.exception( - "Error getting tag value. This message will only be logged every ten minutes." - ) + syslogger.exception("Error getting tag value. This message will only be logged every ten minutes.") # If we can, try to send the exception back whence it came try: from . import newevt if hasattr(active_claim_value, "__module__"): - if ( - active_claim_value.__module__ - in newevt.eventsByModuleName - ): - newevt.eventsByModuleName[ - active_claim_value.__module__ - ]._handle_exception() + if active_claim_value.__module__ in newevt.eventsByModuleName: + newevt.eventsByModuleName[active_claim_value.__module__]._handle_exception() except Exception: print(traceback.format_exc()) @@ -1859,9 +1772,7 @@ def pushOnRepeats(self): @pushOnRepeats.setter def pushOnRepeats(self, v): - raise AttributeError( - "Push on repeats was causing too much trouble and too much confusion and has been removed" - ) + raise AttributeError("Push on repeats was causing too much trouble and too much confusion and has been removed") def handleSourceChanged(self, name): try: @@ -1915,9 +1826,7 @@ def claim( # If the weakref obj disappeared it will be None if claim is None: priority = priority or 50 - claim = self.claimFactory( - value, name, priority, timestamp, annotation, expiration - ) + claim = self.claimFactory(value, name, priority, timestamp, annotation, expiration) else: # It could have been released previously. @@ -1949,11 +1858,7 @@ def claim( claim.vta = value, timestamp, annotation # If we have priortity on them, or if we have the same priority but are newer - if ( - (ac is None) - or (priority > oldAcPriority) - or ((priority == oldAcPriority) and (timestamp > oldAcTimestamp)) - ): + if (ac is None) or (priority > oldAcPriority) or ((priority == oldAcPriority) and (timestamp > oldAcTimestamp)): self.active_claim = self.claims[name] self.handleSourceChanged(name) @@ -2032,9 +1937,7 @@ def set_claim_val( # and are more recent, byt to do that we have to use # the slower claim function that handles creating # and switching claims - if (ac is None) or ( - co.priority >= ac.priority and timestamp >= ac.timestamp - ): + if (ac is None) or (co.priority >= ac.priority and timestamp >= ac.timestamp): self.claim(val, claim, co.priority, timestamp, annotation) return @@ -2083,9 +1986,7 @@ def getTopClaim(self) -> Claim[T]: # Eliminate dead ones x = [i for i in x if i and not i.released] if not x: - raise RuntimeError( - f"Program state is corrupt, tag{self.name} has no claims" - ) + raise RuntimeError(f"Program state is corrupt, tag{self.name} has no claims") # Get the top one x = sorted(x, reverse=True)[0] return x @@ -2243,9 +2144,7 @@ def claimFactory( annotation: Any, expiration: float = 0, ): - return NumericClaim( - self, value, name, priority, timestamp, annotation, expiration - ) + return NumericClaim(self, value, name, priority, timestamp, annotation, expiration) @property def min(self) -> float | int: @@ -2340,9 +2239,7 @@ def unit(self, value: str): if self._unit: if not self._unit == value: if value: - raise ValueError( - "Cannot change unit of tagpoint. To override this, set to None or '' first" - ) + raise ValueError("Cannot change unit of tagpoint. To override this, set to None or '' first") # TODO race condition in between check, but nobody will be setting this from different threads # I don't think if not self._displayUnits: @@ -2770,9 +2667,7 @@ def _managePolling(self): if not self.poller or not (interval == self.poller.interval): if self.poller: self.poller.unregister() - self.poller = scheduling.scheduler.schedule_repeating( - self.expirePoll, interval, sync=False - ) + self.poller = scheduling.scheduler.schedule_repeating(self.expirePoll, interval, sync=False) else: if self.poller: self.poller.unregister() @@ -2826,9 +2721,7 @@ def set(self, value, timestamp: float | None = None, annotation: Any = None): self.tag.lock.release() else: - raise RuntimeError( - "Cannot get lock to re-claim after release, waited 60s" - ) + raise RuntimeError("Cannot get lock to re-claim after release, waited 60s") else: self.tag.set_claim_val(self.name, value, timestamp, annotation) @@ -2890,9 +2783,7 @@ def __init__( expiration: float = 0, ): self.tag: NumericTagPointClass - Claim.__init__( - self, tag, value, name, priority, timestamp, annotation, expiration - ) + Claim.__init__(self, tag, value, name, priority, timestamp, annotation, expiration) def setAs( self, @@ -2954,9 +2845,7 @@ def __init__(self, name, inputTag, timeConstant, priority=60, interval=-1): inputTag.subscribe(self.doInput) self.tag = Tag(name) - self.claim = self.tag.claim( - self.getter, name=f"{inputTag.name}.lowpass", priority=priority - ) + self.claim = self.tag.claim(self.getter, name=f"{inputTag.name}.lowpass", priority=priority) if interval is None: self.tag.interval = timeConstant / 2 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 000000000..1e2f62448 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1675 @@ +[[package]] +name = "aioesphomeapi" +version = "19.3.0" +description = "Python API for interacting with ESPHome devices." +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +async-timeout = {version = ">=4.0", markers = "python_version < \"3.11\""} +chacha20poly1305-reuseable = ">=0.2.5" +noiseprotocol = ">=0.3.1,<1.0" +protobuf = ">=3.19.0" +zeroconf = ">=0.36.0,<1.0" + +[[package]] +name = "apprise" +version = "1.7.5" +description = "Push Notifications that work with just about every platform!" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +certifi = "*" +click = ">=5.0" +markdown = "*" +PyYAML = "*" +requests = "*" +requests-oauthlib = "*" + +[[package]] +name = "astral" +version = "3.2" +description = "Calculations for the position of the sun and moon." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "attr" +version = "0.3.2" +description = "Simple decorator to set attributes of target function or class in a DRY way." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +cov = ["attrs", "coverage[toml] (>=5.3)"] +dev = ["attrs", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs", "cloudpickle", "hypothesis", "pympler", "pytest-xdist", "pytest (>=4.3.0)"] + +[[package]] +name = "autocommand" +version = "2.2.2" +description = "A library to create a command-line program from a function" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "backports.tarfile" +version = "1.0.0" +description = "Backport of CPython tarfile module" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint"] +testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + +[[package]] +name = "blinker" +version = "1.7.0" +description = "Fast, simple object-to-object and broadcast signaling" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chacha20poly1305-reuseable" +version = "0.12.1" +description = "ChaCha20Poly1305 that is reuseable for asyncio" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" + +[package.dependencies] +cryptography = ">=42.0.0" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" + +[[package]] +name = "cheroot" +version = "10.0.0" +description = "Highly-optimized, pure-python HTTP server" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +"jaraco.functools" = "*" +more-itertools = ">=2.6" + +[package.extras] +docs = ["sphinx (>=1.8.2)", "jaraco.packaging (>=3.2)", "sphinx-tabs (>=1.1.0)", "furo", "python-dateutil", "sphinxcontrib-apidoc (>=0.3.0)"] + +[[package]] +name = "cherrypy" +version = "18.9.0" +description = "Object-Oriented HTTP framework" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cheroot = ">=8.2.1" +"jaraco.collections" = "*" +more-itertools = "*" +portend = ">=2.1.1" +"zc.lockfile" = "*" + +[package.extras] +docs = ["sphinx", "docutils", "alabaster", "sphinxcontrib-apidoc (>=0.3.0)", "rst.linker (>=1.11)", "jaraco.packaging (>=3.2)", "setuptools"] +json = ["simplejson"] +memcached_session = ["python-memcached (>=1.58)"] +routes_dispatcher = ["routes (>=2.3.1)"] +ssl = ["pyopenssl"] +testing = ["coverage", "codecov", "objgraph", "pytest (>=5.3.5)", "pytest-cov", "pytest-forked", "pytest-sugar", "path.py", "requests-toolbelt", "pytest-services (>=2)", "setuptools"] +xcgi = ["flup"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "colorzero" +version = "2.0" +description = "Yet another Python color library" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +doc = ["pkginfo", "sphinx", "sphinx-rtd-theme"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "cryptography" +version = "42.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["ruff", "mypy", "check-sdist", "click"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist", "pretend", "certifi"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "evdev" +version = "1.7.0" +description = "Bindings to the Linux input handling subsystem" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +description = "Python bindings for FFmpeg - with complex filtering support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +future = "*" + +[package.extras] +dev = ["future (==0.17.1)", "numpy (==1.16.4)", "pytest-mock (==1.10.4)", "pytest (==4.6.1)", "Sphinx (==2.1.0)", "tox (==3.12.1)"] + +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "gpiozero" +version = "2.0.1" +description = "A simple interface to GPIO devices with Raspberry Pi" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +colorzero = "*" + +[package.extras] +doc = ["sphinx-rtd-theme (>=1.0)", "sphinx (>=4.0)"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "holidays" +version = "0.46" +description = "Generate and work with holidays in Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +python-dateutil = "*" + +[[package]] +name = "icemedia" +version = "0.1.1" +description = "A GStreamer wrapper with JACK connection management" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +JACK-Client = "*" +scullery = "*" + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ifaddr" +version = "0.2.0" +description = "Cross-platform network interface and IP address enumeration library" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "inflect" +version = "7.2.0" +description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +more-itertools = "*" +typeguard = ">=4.0.1" +typing-extensions = "*" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-mypy", "pytest-enabler (>=2.2)", "pytest-ruff (>=0.2.1)", "pygments"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "iot-devices" +version = "0.1.18" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +colorzero = "*" +gpiozero = "*" +paho-mqtt = "*" +scullery = "*" +urwid = "*" +yeelight = "*" + +[[package]] +name = "isodate" +version = "0.6.1" +description = "An ISO 8601 date/time/duration parser and formatter" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "jack-client" +version = "0.5.4" +description = "JACK Audio Connection Kit (JACK) Client for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +CFFI = ">=1.0" + +[package.extras] +numpy = ["numpy"] + +[[package]] +name = "jaraco.collections" +version = "5.0.0" +description = "Collection objects similar to those in stdlib by jaraco" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +"jaraco.text" = "*" + +[package.extras] +docs = ["sphinx (>=3.5)", "sphinx (<7.2.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "jaraco.context" +version = "5.3.0" +description = "Useful decorators and context managers" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-mypy", "pytest-enabler (>=2.2)", "pytest-ruff (>=0.2.1)", "portend"] + +[[package]] +name = "jaraco.functools" +version = "4.0.0" +description = "Functools like those found in stdlib" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["sphinx (>=3.5)", "sphinx (<7.2.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "jaraco.classes", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "jaraco.text" +version = "3.12.0" +description = "Module for text manipulation" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +autocommand = "*" +inflect = "*" +"jaraco.context" = ">=4.1" +"jaraco.functools" = "*" +more-itertools = "*" + +[package.extras] +docs = ["sphinx (>=3.5)", "sphinx (<7.2.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pathlib2"] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "lxml" +version = "5.2.1" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +html-clean = ["lxml-html-clean"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=3.0.10)"] + +[[package]] +name = "mako" +version = "1.3.2" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +testing = ["pytest"] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["mkdocs (>=1.5)", "mkdocs-nature (>=0.6)", "mdx-gh-links (>=0.2)", "mkdocstrings", "mkdocs-gen-files", "mkdocs-section-index", "mkdocs-literate-nav"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "msgpack" +version = "1.0.8" +description = "MessagePack serializer" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "netdisco" +version = "3.0.0" +description = "Discover devices on your local network" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +requests = ">=2.0" +zeroconf = ">=0.30.0" + +[[package]] +name = "netifaces" +version = "0.11.0" +description = "Portable network interface information." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "nmcli" +version = "1.3.0" +description = "A python wrapper library for the network-manager cli client" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "noiseprotocol" +version = "0.3.1" +description = "Implementation of Noise Protocol Framework" +category = "main" +optional = false +python-versions = "~=3.5" + +[package.dependencies] +cryptography = ">=2.8" + +[[package]] +name = "ntplib" +version = "0.4.0" +description = "Python NTP library" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" + +[[package]] +name = "nvrchannel" +version = "0.1.5" +description = "" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "oauthlib" +version = "3.2.2" +description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +rsa = ["cryptography (>=3.0.0)"] +signals = ["blinker (>=1.4.0)"] +signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] + +[[package]] +name = "opencv-python" +version = "4.9.0.80" +description = "Wrapper package for OpenCV python bindings." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = [ + {version = ">=1.21.2", markers = "python_version >= \"3.10\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.19.3", markers = "python_version >= \"3.6\" and platform_system == \"Linux\" and platform_machine == \"aarch64\" or python_version >= \"3.9\""}, + {version = ">=1.17.0", markers = "python_version >= \"3.7\""}, + {version = ">=1.17.3", markers = "python_version >= \"3.8\""}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "paho-mqtt" +version = "1.6.0" +description = "MQTT version 5.0/3.1.1 client class" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +proxy = ["pysocks"] + +[[package]] +name = "pam" +version = "0.2.0" +description = "" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-pam = "*" + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "peewee" +version = "3.17.1" +description = "a little orm" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pillow" +version = "10.3.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "pint" +version = "0.23" +description = "Physical quantities module" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +babel = ["babel (<=2.8)"] +bench = ["pytest", "pytest-codspeed"] +dask = ["dask"] +mip = ["mip (>=1.13)"] +numpy = ["numpy (>=1.19.5)"] +pandas = ["pint-pandas (>=0.3)"] +test = ["pytest", "pytest-mpl", "pytest-cov", "pytest-subtests", "pytest-benchmark"] +testbase = ["pytest", "pytest-cov", "pytest-subtests", "pytest-benchmark"] +uncertainties = ["uncertainties (>=3.1.6)"] +xarray = ["xarray"] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.25.2)", "sphinx (>=7.2.6)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest (>=7.4.3)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "portend" +version = "3.2.0" +description = "TCP port monitoring and discovery" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +tempora = ">=1.8" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "protobuf" +version = "5.26.1" +description = "" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "psutil" +version = "5.9.8" +description = "Cross-platform lib for process and system monitoring in Python." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pygrep" +version = "0.3" +description = "Find python identifiers" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +dev = ["pytest"] + +[[package]] +name = "pynput" +version = "1.7.6" +description = "Monitor and control user input devices" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +evdev = {version = ">=1.3", markers = "sys_platform in \"linux\""} +pyobjc-framework-ApplicationServices = {version = ">=8.0", markers = "sys_platform == \"darwin\""} +pyobjc-framework-Quartz = {version = ">=8.0", markers = "sys_platform == \"darwin\""} +python-xlib = {version = ">=0.17", markers = "sys_platform in \"linux\""} +six = "*" + +[[package]] +name = "pyobjc-core" +version = "10.2" +description = "Python<->ObjC Interoperability Module" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pyobjc-framework-applicationservices" +version = "10.2" +description = "Wrappers for the framework ApplicationServices on macOS" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" +pyobjc-framework-CoreText = ">=10.2" +pyobjc-framework-Quartz = ">=10.2" + +[[package]] +name = "pyobjc-framework-cocoa" +version = "10.2" +description = "Wrappers for the Cocoa frameworks on macOS" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pyobjc-core = ">=10.2" + +[[package]] +name = "pyobjc-framework-coretext" +version = "10.2" +description = "Wrappers for the framework CoreText on macOS" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" +pyobjc-framework-Quartz = ">=10.2" + +[[package]] +name = "pyobjc-framework-quartz" +version = "10.2" +description = "Wrappers for the Quartz frameworks on macOS" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pyobjc-core = ">=10.2" +pyobjc-framework-Cocoa = ">=10.2" + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "python-mpv-jsonipc" +version = "1.2.0" +description = "Python API to MPV using JSON IPC" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "python-pam" +version = "2.0.2" +description = "Python PAM module using ctypes, py3" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-rtmidi" +version = "1.5.0" +description = "A Python binding for the RtMidi C++ library implemented using Cython." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-xlib" +version = "0.33" +description = "Python X Library" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.10.0" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pywinpty" +version = "2.0.13" +description = "Pseudo terminal support for Windows from Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "referencing" +version = "0.34.0" +description = "JSON Referencing + Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-file" +version = "2.0.0" +description = "File transport adapter for Requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=1.0.0" + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +description = "OAuthlib authentication support for Requests." +category = "main" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +oauthlib = ">=3.0.0" +requests = ">=2.0.0" + +[package.extras] +rsa = ["oauthlib[signedtoken] (>=3.0.0)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rpds-py" +version = "0.18.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "scipy" +version = "1.13.0" +description = "Fundamental algorithms for scientific computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +test = ["pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "asv", "mpmath", "gmpy2", "threadpoolctl", "scikit-umfpack", "pooch", "hypothesis (>=6.30)", "array-api-strict"] +doc = ["sphinx (>=5.0.0)", "pydata-sphinx-theme (>=0.15.2)", "sphinx-design (>=0.4.0)", "matplotlib (>=3.5)", "numpydoc", "jupytext", "myst-nb", "pooch", "jupyterlite-sphinx (>=0.12.0)", "jupyterlite-pyodide-kernel"] +dev = ["mypy", "typing-extensions", "types-psutil", "pycodestyle", "ruff", "cython-lint (>=0.12.2)", "rich-click", "doit (>=0.36.0)", "pydevtool"] + +[[package]] +name = "scullery" +version = "0.1.16" +description = "A utility library based on KaithemAutomation featuring a GStreamer wrapper" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +paho-mqtt = "*" +pint = "*" +pyyaml = "*" +typeguard = "*" + +[[package]] +name = "setproctitle" +version = "1.3.3" +description = "A Python module to customize the process title" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "sf2utils" +version = "1.0.0" +description = "Sound font 2 parsing library and utilities" +category = "main" +optional = false +python-versions = ">=2.7" + +[[package]] +name = "simpleeval" +version = "0.9.13" +description = "A simple, safe single expression evaluator library." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tatsu" +version = "5.8.3" +description = "TatSu takes a grammar in a variation of EBNF as input, and outputs a memoizing PEG/Packrat parser in Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +future-regex = ["regex"] + +[[package]] +name = "tempora" +version = "5.5.1" +description = "Objects and routines pertaining to date and time (tempora)" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +"jaraco.functools" = ">=1.20" +pytz = "*" + +[package.extras] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ruff (>=0.2.1)", "pytest-freezer", "types-pytz", "pytest-mypy"] + +[[package]] +name = "terminado" +version = "0.18.1" +description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +ptyprocess = {version = "*", markers = "os_name != \"nt\""} +pywinpty = {version = ">=1.1.0", markers = "os_name == \"nt\""} +tornado = ">=6.1.0" + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["pre-commit", "pytest-timeout", "pytest (>=7.0)"] +typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"] + +[[package]] +name = "textdistance" +version = "4.6.1" +description = "Compute distance between the two texts." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dameraulevenshtein = ["rapidfuzz (>=2.6.0)", "jellyfish", "pyxdameraulevenshtein"] +hamming = ["levenshtein", "rapidfuzz (>=2.6.0)", "jellyfish", "distance"] +jaro = ["rapidfuzz (>=2.6.0)", "levenshtein"] +jarowinkler = ["rapidfuzz (>=2.6.0)", "jellyfish"] +levenshtein = ["rapidfuzz (>=2.6.0)", "levenshtein"] +all = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)", "distance", "pylev", "py-stringmatching", "tabulate"] +benchmark = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)", "distance", "pylev", "py-stringmatching", "tabulate"] +benchmarks = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)", "distance", "pylev", "py-stringmatching", "tabulate"] +common = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)"] +extra = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)"] +extras = ["jellyfish", "numpy", "levenshtein", "pyxdameraulevenshtein", "rapidfuzz (>=2.6.0)"] +lint = ["twine", "mypy", "isort", "flake8", "types-tabulate", "flake8-blind-except", "flake8-bugbear", "flake8-commas", "flake8-logging-format", "flake8-mutable", "flake8-pep3101", "flake8-quotes", "flake8-string-format", "flake8-tidy-imports", "pep8-naming"] +test = ["hypothesis", "isort", "numpy", "pytest"] + +[[package]] +name = "tflite-runtime" +version = "2.14.0" +description = "TensorFlow Lite is for mobile and embedded devices." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = ">=1.23.2" + +[[package]] +name = "tinytag" +version = "1.10.1" +description = "Read music meta data and length of MP3, OGG, OPUS, MP4, M4A, FLAC, WMA and Wave files" +category = "main" +optional = false +python-versions = ">=2.7" + +[package.extras] +tests = ["pytest", "pytest-cov", "flake8"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "main" +optional = false +python-versions = ">= 3.8" + +[[package]] +name = "typeguard" +version = "4.2.1" +description = "Run-time type checker for Python" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["packaging", "Sphinx (>=7)", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=7)", "pytest (>=7)", "mypy (>=1.2.0)"] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20240311" +description = "Typing stubs for PyYAML" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "types-requests" +version = "2.31.0.20240406" +description = "Typing stubs for requests" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" + +[[package]] +name = "upnpclient" +version = "0.0.8" +description = "Python 2 and 3 library for accessing uPnP devices." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +lxml = "*" +netdisco = "*" +python-dateutil = "*" +requests = "*" +six = "*" + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "urwid" +version = "2.6.10" +description = "A full-featured console (xterm et al.) user interface library" +category = "main" +optional = false +python-versions = ">3.7" + +[package.dependencies] +typing-extensions = "*" +wcwidth = "*" + +[package.extras] +curses = ["windows-curses"] +glib = ["pygobject"] +lcd = ["pyserial"] +serial = ["pyserial"] +tornado = ["tornado (>=5.0)"] +trio = ["trio (>=0.22.0)", "exceptiongroup"] +twisted = ["twisted"] +zmq = ["zmq"] + +[[package]] +name = "vignette" +version = "5.1.1" +description = "Library to create FreeDesktop-compatible thumbnails" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-magic = "*" + +[package.extras] +pillow = ["pillow (>=6.0)"] +pyqt6 = ["pyqt6"] +pythonmagick = ["pythonmagick"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "werkzeug" +version = "3.0.2" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "yappi" +version = "1.6.0" +description = "Yet Another Python Profiler" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["gevent (>=20.6.2)"] + +[[package]] +name = "yeelight" +version = "0.7.14" +description = "A Python library for controlling YeeLight RGB bulbs." +category = "main" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +async-timeout = {version = "*", markers = "python_version < \"3.11\""} +future = "*" +ifaddr = "*" + +[package.extras] +dev = ["flake8", "flake8-docstrings", "flake8-import-order", "flake8-tidy-imports", "pep8-naming", "sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "zc.lockfile" +version = "3.0.post1" +description = "Basic inter-process locks" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["zope.testing"] + +[[package]] +name = "zeep" +version = "4.2.1" +description = "A Python SOAP client" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=17.2.0" +isodate = ">=0.5.4" +lxml = ">=4.6.0" +platformdirs = ">=1.4.0" +pytz = "*" +requests = ">=2.7.0" +requests-file = ">=1.5.1" +requests-toolbelt = ">=0.7.1" + +[package.extras] +async = ["httpx (>=0.15.0)"] +docs = ["sphinx (>=1.4.0)"] +test = ["coverage[toml] (==5.2.1)", "freezegun (==0.3.15)", "pretend (==1.0.9)", "pytest-cov (==2.8.1)", "pytest-httpx", "pytest-asyncio", "pytest (==6.2.5)", "requests-mock (>=0.7.0)", "isort (==5.3.2)", "flake8 (==3.8.3)", "flake8-blind-except (==0.1.1)", "flake8-debugger (==3.2.1)", "flake8-imports (==0.1.1)"] +xmlsec = ["xmlsec (>=0.6.1)"] + +[[package]] +name = "zeroconf" +version = "0.119.0" +description = "A pure python implementation of multicast DNS service discovery" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +async-timeout = {version = ">=3.0.0", markers = "python_version < \"3.11\""} +ifaddr = ">=0.1.7" + +[metadata] +lock-version = "1.1" + python-versions = "^3.10" +content-hash = "fe983d30de121c1d5af1b25a684dee7ae0f1c2c0d7d9d5bfe20f3b0dd4c82fad" + +[metadata.files] +aioesphomeapi = [] +apprise = [] +astral = [] +async-timeout = [] +attr = [] +attrs = [] +autocommand = [] +"backports.tarfile" = [] +blinker = [] +certifi = [] +cffi = [] +chacha20poly1305-reuseable = [] +charset-normalizer = [] +cheroot = [] +cherrypy = [] +click = [] +colorama = [] +colorzero = [] +cryptography = [] +evdev = [] +exceptiongroup = [] +ffmpeg-python = [] +flask = [] +future = [] +gpiozero = [] +holidays = [] +icemedia = [] +idna = [] +ifaddr = [] +inflect = [] +iniconfig = [] +iot-devices = [] +isodate = [] +itsdangerous = [] +jack-client = [] +"jaraco.collections" = [] +"jaraco.context" = [] +"jaraco.functools" = [] +"jaraco.text" = [] +jinja2 = [] +jsonschema = [] +jsonschema-specifications = [] +lxml = [] +mako = [] +markdown = [] +markupsafe = [] +more-itertools = [] +msgpack = [] +netdisco = [] +netifaces = [] +nmcli = [] +noiseprotocol = [] +ntplib = [] +numpy = [] +nvrchannel = [] +oauthlib = [] +opencv-python = [] +packaging = [] +paho-mqtt = [] +pam = [] +passlib = [] +peewee = [] +pillow = [] +pint = [] +platformdirs = [] +pluggy = [] +portend = [] +protobuf = [] +psutil = [] +ptyprocess = [] +pycparser = [] +pyflakes = [] +pygments = [] +pygrep = [] +pynput = [] +pyobjc-core = [] +pyobjc-framework-applicationservices = [] +pyobjc-framework-cocoa = [] +pyobjc-framework-coretext = [] +pyobjc-framework-quartz = [] +pyserial = [] +pytest = [] +python-dateutil = [] +python-magic = [] +python-mpv-jsonipc = [] +python-pam = [] +python-rtmidi = [] +python-xlib = [] +pytz = [] +pywinpty = [] +pyyaml = [] +referencing = [] +requests = [] +requests-file = [] +requests-oauthlib = [] +requests-toolbelt = [] +rpds-py = [] +scipy = [] +scullery = [] +setproctitle = [] +sf2utils = [] +simpleeval = [] +six = [] +tatsu = [] +tempora = [] +terminado = [] +textdistance = [] +tflite-runtime = [] +tinytag = [] +toml = [] +tomli = [] +tornado = [] +typeguard = [] +types-pyyaml = [] +types-requests = [] +typing-extensions = [] +tzdata = [] +upnpclient = [] +urllib3 = [] +urwid = [] +vignette = [] +wcwidth = [] +websockets = [] +werkzeug = [] +yappi = [] +yeelight = [] +"zc.lockfile" = [] +zeep = [] +zeroconf = [] diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 000000000..358311f11 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs.options] +system-site-packages = false diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..473bf5aa8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,102 @@ +[tool.poetry] + name = "kaithem" + version = "0.78.0" + description = "Home/commercial automation server" + license = "GPL-3.0-only" + packages = [{include = "kaithem"}] + authors = ["Daniel Dunn"] + readme = "README.md" + +[tool.ruff] + line-length = 140 + +[tool.poetry.dependencies] + python = "^3.10" + # First party + scullery = "^0.1.16" + iot-devices= "*" + icemedia= "*" + NVRChannel= "*" + + # NVRChannel didn't bring this in 0.15.0 + opencv-python= "*" + + numpy="^1.26.1" + cherrypy = "*" + cheroot= "*" + flask= "*" + tornado= "*" + mako= "*" + jinja2= "*" + astral= "*" + tatsu= "*" + pam= "*" + msgpack= "*" + pyyaml= "*" + types-PyYaml= "*" + pytest= "*" + nmcli= "*" + peewee= "*" + terminado= "*" + apprise= "*" + + ffmpeg-python= "*" + yappi= "*" + zeroconf="^0.119.0" + colorzero= "*" + # Still included in repo because it doesn't install correctly due to + # suds-passworddigest + #onvif + typeguard= "*" + tinytag= "*" + jsonschema= "*" + pint= "*" + pyflakes= "*" + python_mpv_jsonipc= "*" + textdistance= "*" + toml= "*" + vignette= "*" + simpleeval= "*" + websockets= "*" + zeep= "*" + passlib= "*" + Pillow= "*" + tflite-runtime= "*" + evdev= "*" + attr= "*" + markupsafe= "*" + upnpclient= "*" + requests= "*" + types-requests= "*" + python-dateutil= "*" + pygments= "*" + pytz= "*" + ntplib= "*" + holidays= "*" + + yeelight= "*" + + pyserial= "*" + pygrep= "*" + python-rtmidi= "1.5.0" + paho-mqtt = "<=1.6.0" + setproctitle= "*" + psutil= "*" + netifaces= "*" + JACK-Client= "*" + aioesphomeapi= "*" + sf2utils= "*" + pynput= "*" + + # Older is not compatible with new numpy + scipy = ">=1.11.0" + +[tool.poetry.scripts] + "kaithem" = "kaithem:__main__" + "kaithem._jackmanager_server" = "icemedia.jack_client_subprocess:main" + "kaithem._iceflow_server" = "icemedia.iceflow_server:main" + + +[build-system] + requires = ["poetry-core"] + build-backend = "poetry.core.masonry.api" diff --git a/requirements.txt b/requirements.txt index be0ba8127..205f0ad0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aioesphomeapi==23.2.0 +aioesphomeapi==24.0.0 aiohappyeyeballs==2.3.2 annotated-types==0.6.0 apprise==1.7.5 @@ -22,7 +22,7 @@ cryptography==42.0.5 evdev==1.7.0 exceptiongroup==1.2.0 ffmpeg-python==0.2.0 -Flask==3.0.2 +Flask==3.0.3 future==1.0.0 gpiozero==2.0.1 holidays==0.46 @@ -55,6 +55,7 @@ ntplib==0.4.0 numpy==1.26.4 NVRChannel==0.1.5 oauthlib==3.2.2 +opencv-python==4.9.0.80 packaging==24.0 paho-mqtt==1.6.0 pam==0.2.0 @@ -94,7 +95,7 @@ requests-oauthlib==2.0.0 requests-toolbelt==1.0.0 rpds-py==0.18.0 scipy==1.13.0 -scullery==0.1.15 +scullery==0.1.16 setproctitle==1.3.3 sf2utils==1.0.0 simpleeval==0.9.13 diff --git a/scripts/install-kaithem-service.sh b/scripts/install-kaithem-service.sh new file mode 100644 index 000000000..7e8fe1012 --- /dev/null +++ b/scripts/install-kaithem-service.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +## Install Kaithem, with all optional features. +## Runs as your user +## This doesn't install system dependencies, it is meant to be run from the Makefile + +############################################################################################################## + +set -x +set -e + +mkdir -p ~/kaithem +pipx install poetry +pipx install --system-site-packages --force . + + +cat << EOF > ~/kaithem/config.yaml +# Add your config here! + +EOF + + +mkdir -p ~/.config/systemd/user/ +cat << EOF > ~/.config/systemd/user/kaithem.service +[Unit] +Description=KaithemAutomation python based automation server +After=time-sync.target sysinit.service mosquitto.service zigbee2mqtt.service pipewire.service multi-user.target graphical.target pipewire-media-session.service wireplumber.service + + +[Service] +TimeoutStartSec=0 +ExecStart=/usr/bin/pw-jack kaithem +Restart=on-failure +RestartSec=15 +Type=simple + +[Install] +WantedBy=default.target +EOF + +systemctl --user enable --now kaithem.service diff --git a/scripts/install-kaithem.sh b/scripts/install-kaithem.sh deleted file mode 100644 index aa0506ba3..000000000 --- a/scripts/install-kaithem.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -## Install Kaithem, with all optional features. -## Runs as your user -## This doesn't install system dependencies, it is meant to be run from the Makefile - -############################################################################################################## - -# Don't need to install any system packages, the makefile does it for us. - -set -x -set -e - -mkdir -p ~/kaithem - -! deactivate - -if [ ! -d ~/kaithem/.venv ]; then - virtualenv --system-site-packages ~/kaithem/.venv -else - echo "Found venv" -fi - -~/kaithem/.venv/bin/python -m pip install --upgrade -r requirements.txt -! ~/kaithem/.venv/bin/python -m pip uninstall kaithem -y -~/kaithem/.venv/bin/python setup.py install --force - - -cat << EOF > ~/kaithem/config.yaml -site-data-dir: ~/kaithem -ssl-dir: ~/kaithem/ssl -save-before-shutdown: yes -autosave-state: 2 hours -worker-threads: 16 -http-thread-pool: 16 -https-thread-pool: 4 - -#The port on which web pages will be served. The default port is 443, but we use 8001 in case you are running apache or something. -https-port : 8001 -#The port on which unencrypted web pages will be served. The default port is 80, but we use 8001 in case you are running apache or something. -http-port : 8002 - -audio-paths: - - __default__ -EOF - - -mkdir -p ~/.config/systemd/user/ -cat << EOF > ~/.config/systemd/user/kaithem.service -[Unit] -Description=KaithemAutomation python based automation server -After=time-sync.target sysinit.service mosquitto.service zigbee2mqtt.service pipewire.service multi-user.target graphical.target pipewire-media-session.service wireplumber.service - - -[Service] -TimeoutStartSec=0 -ExecStart=/usr/bin/pw-jack /home/%u/kaithem/.venv/bin/python -m kaithem -c /home/%u/kaithem/config.yaml -Restart=on-failure -RestartSec=15 -Type=simple - -[Install] -WantedBy=default.target -EOF - -systemctl --user enable --now kaithem.service diff --git a/setup.py b/setup.py deleted file mode 100644 index fcdaf1e09..000000000 --- a/setup.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import re -from setuptools import setup, find_packages - -# Utility function to read the README file. -# Used for the long_description. It's nice, because now 1) we have a top level -# README file and 2) it's easier to type in the README file than to put a raw -# string in below ... - - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -# https://stackoverflow.com/questions/458550/standard-way-to-embed-version-into-python-package -VERSIONFILE = "kaithem/__version__.py" -verstrline = open(VERSIONFILE).read() -VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" -mo = re.search(VSRE, verstrline, re.M) -if mo: - verstr = mo.group(1) -else: - raise RuntimeError(f"Unable to find version string in {VERSIONFILE}.") - - -def package_files(directory, ext=""): - paths = [] - for path, directories, filenames in os.walk(directory): - for filename in filenames: - if filename.endswith(ext): - paths.append(os.path.join("..", path, filename)) - return paths - - -with open("requirements.txt") as f: - frozen_requirements = f.read().splitlines() - - -extra_files = ( - package_files("kaithem/data/") - + package_files("kaithem/src/", "html") - + package_files("kaithem/src/", "js") - + package_files("kaithem/src/", "css") -) -setup( - name="kaithem", - version=verstr, - author="Daniel Dunn", - author_email="danny@example.com", - description=("Home/Commercial automation server"), - license="GPLv3", - keywords="automation", - url="https://github.com/EternityForest/KaithemAutomation", - packages=find_packages(), - package_data={ - "": extra_files - + [ - "**/*.txt", - "**/*.yaml", - "**/*.html", - "**/*.md", - "**/*.json", - "**/*.js", - "**/*.css", - "**/*.vue", - "**/*.webp", - "**/*.avif", - "**/*.png", - "**/*.jpg", - "**/*.toml", - "**/*.svg", - "**/*.opus", - "**/*.ogg", - "**/*.mp3", - "**/*.tflite", - ] - }, - long_description=read("README.md"), - classifiers=[ - "Development Status :: 3 - Alpha", - "Topic :: Utilities", - "License :: OSI Approved :: GPLv3 License", - ], - entry_points={ - "console_scripts": [ - "kaithem = kaithem:start", - "kaithem._jackmanager_server = scullery.jack_client_subprocess:main", - "kaithem._iceflow_server = icemedia.iceflow_server:main", - ], - }, - python_requires=">=3.10", - install_requires=frozen_requirements, -)