From 396e5916a2f948bee9274572da3b71f44095c0a3 Mon Sep 17 00:00:00 2001 From: Thiago Macedo Date: Fri, 15 Mar 2019 20:40:02 -0300 Subject: [PATCH 1/9] documenting python3 requirement --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index 4d11f27..0b6e83b 100644 --- a/readme.md +++ b/readme.md @@ -42,3 +42,7 @@ There's a systemd service file ready for use. # check the service sudo systemctl status app-timer ``` + +You must edit the file `config.yaml` for your timers setup. + +In the code there is some python 3 so this is required. For testing the timer and check it's outputs/configs, run `python3 timer.py`. From 2ca8ad521743c56211c5dea43daaa0ae444aa3bf Mon Sep 17 00:00:00 2001 From: randogoth Date: Wed, 12 Nov 2025 23:19:09 +0200 Subject: [PATCH 2/9] added warning capabilities --- config.yaml | 14 +++++++-- lib/__init__.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++--- readme.md | 15 +++++++++ timer.py | 1 + 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/config.yaml b/config.yaml index d67fa98..50b80a8 100644 --- a/config.yaml +++ b/config.yaml @@ -2,12 +2,20 @@ check-interval: 1 timers: gaming: apps: - - retroarch - minecraft + - xmcl + - mcpelauncher-ui # how many minutes of usage - time-limit: 40 + time-limit: 5 # within how many hours of interval - limit-interval: 12 + limit-interval: 4 + # warn n minutes before reaching the limit + warn-threshold: 4 + # commands run on GNOME to pre-warn/final-warn + warn-command: > + runuser -l randogoth -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "App Timer" "Gaming time is almost up ({time_left_int} min left)" -u critical && canberra-gtk-play -i complete' + final-warn-command: > + runuser -l randogoth -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "App Timer" "Final minute before shutdown ({time_left_seconds}s)" -u critical && canberra-gtk-play -i alarm-clock-elapsed' websurf: apps: - chrome diff --git a/lib/__init__.py b/lib/__init__.py index bb236a0..bcb8c92 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -1,3 +1,4 @@ +import math import os, time import subprocess @@ -27,6 +28,7 @@ class Usage(object): super(Usage, self).__init__() self.timer = timer self.file = '%s/usage/%s' % (CONFIG_PATH, self.timer.name) + self.final_warning_sent = False @property def current(self): @@ -45,7 +47,9 @@ class Usage(object): def release(self): '''Remove the timer usage to restart the counter''' - os.remove(self.file) + if os.path.exists(self.file): + os.remove(self.file) + self.final_warning_sent = False def isOffLimit(self): '''Is the timer usage off its limits''' @@ -99,18 +103,89 @@ class Timer(object): apps = [app.strip() for app in item_apps] return apps + @property + def warnThreshold(self): + threshold = self.item.get('warn-threshold', None) + if threshold is None: + return None + return float(threshold) + + @property + def warnCommand(self): + return self.item.get('warn-command') + + @property + def finalWarnCommand(self): + return self.item.get('final-warn-command') + + def _command_context(self, time_left): + minutes = max(time_left, 0) + seconds = minutes * 60 + return { + 'timer_name': self.name, + 'time_left': minutes, + 'time_left_int': int(math.ceil(minutes)), + 'time_left_floor': int(math.floor(minutes)), + 'time_left_seconds': int(math.ceil(seconds)), + } + + def _prepareCommand(self, command, context): + if isinstance(command, (list, tuple)): + command = ' '.join([str(part) for part in command]) + if not command: + return None + context = context or {} + try: + return command.format(**context) + except (KeyError, IndexError, ValueError) as exc: + print('Timer %s warning command format error (%s), using raw command' % (self.name, exc)) + return command + + def _runCommand(self, command, label, context=None): + cmd = self._prepareCommand(command, context) + if not cmd: + return + print('Timer %s running %s command: %s' % (self.name, label, cmd)) + shell(cmd) + + def maybeWarn(self, check_interval): + if self.timeLimit < 0: + return + current_usage = self.usage.current + time_left = self.timeLimit - current_usage + if time_left <= 0: + if self.usage.final_warning_sent: + self.usage.final_warning_sent = False + return + context = self._command_context(time_left) + if (self.warnThreshold is not None and self.warnCommand and + time_left <= self.warnThreshold): + self._runCommand(self.warnCommand, 'warning', context) + final_command = self.finalWarnCommand + if not final_command: + return + interval = float(check_interval) + if time_left <= interval: + if not self.usage.final_warning_sent: + self._runCommand(final_command, 'final warning', context) + self.usage.final_warning_sent = True + else: + # reset flag if the next loop is no longer the final one + if self.usage.final_warning_sent: + self.usage.final_warning_sent = False + def isRunning(self): running = False for app in self.apps: - cmd = 'ps aux | grep -v grep | grep "%s"' % app + cmd = 'pgrep -f "%s"' % app res = shell(cmd) - if len(res): + if res.strip(): running = True return running def block(self): for app in self.apps: - cmd = 'killall "%s"' % app + cmd = 'pkill -f "%s"' % app shell(cmd) diff --git a/readme.md b/readme.md index 0b6e83b..fcc2935 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,21 @@ timers: time-limit: 60 # within how many hours of interval limit-interval: 12 + # optional: warn N minutes before the limit is reached + warn-threshold: 5 + # run on every loop inside the warning window (anything executable) + warn-command: "notify-send 'App Timer' 'Only {time_left_int} minutes left'" + # run only on the final loop before apps are blocked + final-warn-command: "notify-send 'App Timer' 'Last minute before shutdown'" +``` + +`warn-threshold` is measured in minutes and triggers the `warn-command` every loop while there is still time remaining. `final-warn-command` fires once on the last loop before the timer exceeds its limit (based on `check-interval`). All commands are executed through the shell, so they can show desktop notifications, play sounds, etc. You can interpolate values in the command string using `{timer_name}`, `{time_left}`, `{time_left_int}`, `{time_left_floor}`, and `{time_left_seconds}` (any Python format specifiers are supported, e.g. `{time_left:.1f}`). + +If the systemd service runs as `root`, wrap desktop commands so they execute inside your session: + +```yaml +warn-command: > + runuser -l randogoth -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "App Timer" "{timer_name}: {time_left_int} minutes left"' ``` ## Installation diff --git a/timer.py b/timer.py index 28ba190..26047bd 100644 --- a/timer.py +++ b/timer.py @@ -12,6 +12,7 @@ def check_timers(config): if not timer.isRunning(): continue print('Timer %s is running' % timer.name) + timer.maybeWarn(config.checkInterval) # check for off limit apps if timer.usage.isOffLimit(): print('Timer %s is off limit' % timer.name) From ff9ed1dc6f6ccdae94a0479d23038eb83489321c Mon Sep 17 00:00:00 2001 From: Flux Date: Wed, 12 Nov 2025 23:20:39 +0200 Subject: [PATCH 3/9] Update time limits and warning thresholds in config --- config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config.yaml b/config.yaml index 50b80a8..d1fd2c1 100644 --- a/config.yaml +++ b/config.yaml @@ -6,11 +6,11 @@ timers: - xmcl - mcpelauncher-ui # how many minutes of usage - time-limit: 5 + time-limit: 120 # within how many hours of interval - limit-interval: 4 + limit-interval: 12 # warn n minutes before reaching the limit - warn-threshold: 4 + warn-threshold: 5 # commands run on GNOME to pre-warn/final-warn warn-command: > runuser -l randogoth -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "App Timer" "Gaming time is almost up ({time_left_int} min left)" -u critical && canberra-gtk-play -i complete' From 5137b197d71db5f623f06528aae7d3def7e4bcde Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 13 Nov 2025 15:13:58 +0200 Subject: [PATCH 4/9] Add HTML status server --- lib/__init__.py | 30 ++++++-- timer.py | 178 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/lib/__init__.py b/lib/__init__.py index bcb8c92..aa37724 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -61,14 +61,34 @@ class Usage(object): def isOffInterval(self): '''Is the timer usage expired''' + usage_expiry = self.intervalResetTimestamp() + if usage_expiry is None: + return False + return time.time() >= usage_expiry + + def usageStartTimestamp(self): + '''Start timestamp (ctime) for the current interval''' + if not os.path.exists(self.file): + return None + return os.path.getctime(self.file) + + def intervalResetTimestamp(self): + '''Epoch timestamp when the usage interval resets''' limit_interval = self.timer.limitInterval if (limit_interval < 0): - return False - if not os.path.exists(self.file): - return False - usage_started = os.path.getctime(self.file) + return None + usage_started = self.usageStartTimestamp() + if usage_started is None: + return None usage_expiry = usage_started + (limit_interval * 60 * 60) - return time.time() >= usage_expiry + return usage_expiry + + def timeUntilIntervalReset(self): + '''Seconds remaining until the interval resets''' + usage_expiry = self.intervalResetTimestamp() + if usage_expiry is None: + return None + return max(0, usage_expiry - time.time()) class Timer(object): diff --git a/timer.py b/timer.py index 26047bd..20859b0 100644 --- a/timer.py +++ b/timer.py @@ -1,7 +1,182 @@ +import html import os, time +import threading +from datetime import datetime +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from lib import Config +STATUS_SERVER_HOST = os.environ.get('APP_TIMER_STATUS_HOST', '127.0.0.1') +STATUS_SERVER_PORT = int(os.environ.get('APP_TIMER_STATUS_PORT', '8090')) + +STATUS_CONTEXT = {'config': None} + + +def _format_duration(seconds): + if seconds is None: + return '—' + seconds = int(seconds) + hours, remainder = divmod(seconds, 3600) + minutes, _ = divmod(remainder, 60) + parts = [] + if hours: + parts.append('%dh' % hours) + if minutes or not parts: + parts.append('%dm' % minutes) + return ' '.join(parts) + + +def _format_recharge(hours_value, reset_seconds): + if hours_value is None: + return '—' + if float(hours_value).is_integer(): + base = '%dh' % int(hours_value) + else: + base = '%.1fh' % float(hours_value) + if reset_seconds is None: + return 'Every %s' % base + return 'Every %s (ready in %s)' % (base, _format_duration(reset_seconds)) + + +def _format_usage(used, limit): + if limit is None: + return '%d min (no limit)' % used + used = max(0, used) + return '%d / %d min' % (used, limit) + + +def _format_time_left(time_left, limit): + if limit is None or time_left is None: + return '—' + return '%d min' % max(0, int(time_left)) + + +def _collect_timer_snapshot(timer): + usage_minutes = timer.usage.current + limit_minutes = timer.timeLimit + if limit_minutes < 0: + limit_minutes = None + time_left = None + if limit_minutes is not None: + time_left = max(limit_minutes - usage_minutes, 0) + interval_hours = timer.limitInterval + if interval_hours < 0: + interval_hours = None + reset_seconds = timer.usage.timeUntilIntervalReset() if interval_hours is not None else None + running = timer.isRunning() + blocked = bool(timer.usage.isOffLimit()) + return { + 'name': timer.name, + 'usage': usage_minutes, + 'limit': limit_minutes, + 'time_left': time_left, + 'interval_hours': interval_hours, + 'reset_in': reset_seconds, + 'running': running, + 'blocked': blocked, + 'apps': timer.apps, + } + + +def _render_timer_row(snapshot): + usage_text = _format_usage(snapshot['usage'], snapshot['limit']) + time_left_text = _format_time_left(snapshot['time_left'], snapshot['limit']) + interval_text = _format_recharge(snapshot['interval_hours'], snapshot['reset_in']) + active_text = 'Yes' if snapshot['running'] else 'No' + blocked_text = 'Yes' if snapshot['blocked'] else 'No' + apps = snapshot['apps'] or [] + apps_text = ', '.join(html.escape(app) for app in apps) if apps else '—' + return '{name}{usage}{time_left}{interval}{active}{blocked}{apps}'.format( + name=html.escape(snapshot['name']), + usage=usage_text, + time_left=time_left_text, + interval=interval_text, + active=active_text, + blocked=blocked_text, + apps=apps_text, + ) + + +def _render_status_page(config): + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + timers = config.timers if config else [] + rows = ''.join(_render_timer_row(_collect_timer_snapshot(timer)) for timer in timers) + if not rows: + rows = 'No timers configured' + html_doc = """ + + + + App Timer Status + + + +

App Timer Usage

+

Updated {timestamp}

+ + + + + + + + + + + + + + {rows} + +
CategoryUsageTime LeftRechargeActiveBlockedApps
+ +""".format(timestamp=timestamp, rows=rows) + return html_doc + + +class StatusRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path not in ('/', '/status'): + self.send_error(404, 'Not Found') + return + config = STATUS_CONTEXT.get('config') + try: + payload = _render_status_page(config).encode('utf-8') + except Exception as exc: + message = '

Error

%s

' % html.escape(str(exc)) + payload = message.encode('utf-8') + self.send_response(500) + else: + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Content-Length', str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, format, *args): + # Keep HTTP logs concise + print("HTTP %s - %s" % (self.log_date_time_string(), format % args)) + + +def start_status_server(): + try: + httpd = ThreadingHTTPServer((STATUS_SERVER_HOST, STATUS_SERVER_PORT), StatusRequestHandler) + except OSError as exc: + print('Failed to start status server on %s:%s (%s)' % (STATUS_SERVER_HOST, STATUS_SERVER_PORT, exc)) + return None + thread = threading.Thread(target=httpd.serve_forever, daemon=True) + thread.start() + print('Status server running on http://%s:%s' % (STATUS_SERVER_HOST, STATUS_SERVER_PORT)) + return httpd + def check_timers(config): '''Will check every timer setup for it's usage and limits''' for timer in config.timers: @@ -20,8 +195,9 @@ def check_timers(config): # increment running apps timer timer.usage.increment(config.checkInterval) - config = Config() +STATUS_CONTEXT['config'] = config +status_server = start_status_server() while True: # check config changes if config.hasChanges(): From e9bc00b1c8a00d8d1fcdecc98cd45e8de78c5b58 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 13 Nov 2025 15:22:30 +0200 Subject: [PATCH 5/9] updated readme --- config.yaml | 3 +++ lib/__init__.py | 18 ++++++++++++++++++ readme.md | 6 ++++++ timer.py | 31 +++++++++++++++++++++++-------- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/config.yaml b/config.yaml index d1fd2c1..cc4bb43 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,7 @@ check-interval: 1 +status-server: + host: 127.0.0.1 + port: 8090 timers: gaming: apps: diff --git a/lib/__init__.py b/lib/__init__.py index aa37724..982709b 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -253,3 +253,21 @@ class Config(object): '''How often the usage check should happen''' interval = self.data.get('check-interval', 1) return int(interval) + + @property + def statusServer(self): + '''Optional host/port overrides for the embedded status server''' + config = self.data.get('status-server') or {} + host = config.get('host') + port = config.get('port') + if port is None: + parsed_port = None + else: + try: + parsed_port = int(port) + except (TypeError, ValueError): + raise ValueError('status-server.port must be an integer') + return { + 'host': host, + 'port': parsed_port, + } diff --git a/readme.md b/readme.md index fcc2935..b12e70a 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,10 @@ I'm using this at a raspberry pi gaming station (retropie) to enforce usage limi ```yaml # how often should the script check for apps runtime? check-interval: 1 +# optional http status endpoint configuration +status-server: + host: 127.0.0.1 + port: 8090 # timers: list of apps that will be watched timers: # the app name (list key) @@ -40,6 +44,8 @@ warn-command: > runuser -l randogoth -c 'DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "App Timer" "{timer_name}: {time_left_int} minutes left"' ``` +The status server shows timer usage at `http://host:port/` (defaults `127.0.0.1:8090`). Override the bind address or port via the `status-server` block, or keep using the `APP_TIMER_STATUS_HOST` / `APP_TIMER_STATUS_PORT` environment variables if you prefer exporting them in your service unit. + ## Installation There's a systemd service file ready for use. diff --git a/timer.py b/timer.py index 20859b0..6fbcf1c 100644 --- a/timer.py +++ b/timer.py @@ -6,9 +6,6 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from lib import Config -STATUS_SERVER_HOST = os.environ.get('APP_TIMER_STATUS_HOST', '127.0.0.1') -STATUS_SERVER_PORT = int(os.environ.get('APP_TIMER_STATUS_PORT', '8090')) - STATUS_CONTEXT = {'config': None} @@ -166,15 +163,32 @@ class StatusRequestHandler(BaseHTTPRequestHandler): print("HTTP %s - %s" % (self.log_date_time_string(), format % args)) -def start_status_server(): +def _resolve_status_server_bindings(config): + server_config = getattr(config, 'statusServer', {}) or {} + env_host = os.environ.get('APP_TIMER_STATUS_HOST') + env_port = os.environ.get('APP_TIMER_STATUS_PORT') + host = env_host or server_config.get('host') or '127.0.0.1' + if env_port is not None: + try: + port = int(env_port) + except ValueError: + raise ValueError('APP_TIMER_STATUS_PORT must be an integer') + elif server_config.get('port') is not None: + port = server_config['port'] + else: + port = 8090 + return host, port + + +def start_status_server(host, port): try: - httpd = ThreadingHTTPServer((STATUS_SERVER_HOST, STATUS_SERVER_PORT), StatusRequestHandler) + httpd = ThreadingHTTPServer((host, port), StatusRequestHandler) except OSError as exc: - print('Failed to start status server on %s:%s (%s)' % (STATUS_SERVER_HOST, STATUS_SERVER_PORT, exc)) + print('Failed to start status server on %s:%s (%s)' % (host, port, exc)) return None thread = threading.Thread(target=httpd.serve_forever, daemon=True) thread.start() - print('Status server running on http://%s:%s' % (STATUS_SERVER_HOST, STATUS_SERVER_PORT)) + print('Status server running on http://%s:%s' % (host, port)) return httpd def check_timers(config): @@ -197,7 +211,8 @@ def check_timers(config): config = Config() STATUS_CONTEXT['config'] = config -status_server = start_status_server() +status_host, status_port = _resolve_status_server_bindings(config) +status_server = start_status_server(status_host, status_port) while True: # check config changes if config.hasChanges(): From a99d6457742a6bced893b3ab3dfb1f8344eb337d Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 13 Nov 2025 15:28:50 +0200 Subject: [PATCH 6/9] nicer web UI --- config.yaml | 2 +- timer.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/config.yaml b/config.yaml index cc4bb43..688349f 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ check-interval: 1 status-server: host: 127.0.0.1 - port: 8090 + port: 8091 timers: gaming: apps: diff --git a/timer.py b/timer.py index 6fbcf1c..5a905db 100644 --- a/timer.py +++ b/timer.py @@ -106,13 +106,55 @@ def _render_status_page(config): App Timer Status From 3a2420d9b6f006efc7fbc714b4f0814e73bae226 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 13 Nov 2025 16:45:49 +0200 Subject: [PATCH 7/9] fixes, hour format --- lib/__init__.py | 30 +++++++++++++++--------------- timer.py | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/lib/__init__.py b/lib/__init__.py index 982709b..b3c2ec0 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -51,21 +51,6 @@ class Usage(object): os.remove(self.file) self.final_warning_sent = False - def isOffLimit(self): - '''Is the timer usage off its limits''' - time_limit = self.timer.timeLimit - if (time_limit < 0): - return False - if self.current > time_limit: - return True - - def isOffInterval(self): - '''Is the timer usage expired''' - usage_expiry = self.intervalResetTimestamp() - if usage_expiry is None: - return False - return time.time() >= usage_expiry - def usageStartTimestamp(self): '''Start timestamp (ctime) for the current interval''' if not os.path.exists(self.file): @@ -90,6 +75,21 @@ class Usage(object): return None return max(0, usage_expiry - time.time()) + def isOffLimit(self): + '''Is the timer usage off its limits''' + time_limit = self.timer.timeLimit + if (time_limit < 0): + return False + if self.current > time_limit: + return True + + def isOffInterval(self): + '''Is the timer usage expired''' + usage_expiry = self.intervalResetTimestamp() + if usage_expiry is None: + return False + return time.time() >= usage_expiry + class Timer(object): """ diff --git a/timer.py b/timer.py index 5a905db..b4ae358 100644 --- a/timer.py +++ b/timer.py @@ -23,6 +23,22 @@ def _format_duration(seconds): return ' '.join(parts) +def _format_minutes(minutes): + if minutes is None: + return '—' + try: + total_minutes = max(0, int(minutes)) + except (TypeError, ValueError): + return '—' + hours, remainder = divmod(total_minutes, 60) + parts = [] + if hours: + parts.append('%dh' % hours) + if remainder or not parts: + parts.append('%dm' % remainder) + return ' '.join(parts) + + def _format_recharge(hours_value, reset_seconds): if hours_value is None: return '—' @@ -37,15 +53,15 @@ def _format_recharge(hours_value, reset_seconds): def _format_usage(used, limit): if limit is None: - return '%d min (no limit)' % used + return '%s (no limit)' % _format_minutes(used) used = max(0, used) - return '%d / %d min' % (used, limit) + return '%s / %s' % (_format_minutes(used), _format_minutes(limit)) def _format_time_left(time_left, limit): if limit is None or time_left is None: return '—' - return '%d min' % max(0, int(time_left)) + return _format_minutes(max(0, int(time_left))) def _collect_timer_snapshot(timer): @@ -206,7 +222,14 @@ class StatusRequestHandler(BaseHTTPRequestHandler): def _resolve_status_server_bindings(config): - server_config = getattr(config, 'statusServer', {}) or {} + raw_server_config = getattr(config, 'statusServer', None) + config_data = getattr(config, 'data', None) + if raw_server_config: + server_config = raw_server_config or {} + elif isinstance(config_data, dict): + server_config = config_data.get('status-server') or {} + else: + server_config = {} env_host = os.environ.get('APP_TIMER_STATUS_HOST') env_port = os.environ.get('APP_TIMER_STATUS_PORT') host = env_host or server_config.get('host') or '127.0.0.1' @@ -215,10 +238,15 @@ def _resolve_status_server_bindings(config): port = int(env_port) except ValueError: raise ValueError('APP_TIMER_STATUS_PORT must be an integer') - elif server_config.get('port') is not None: - port = server_config['port'] else: - port = 8090 + server_port = server_config.get('port') + if server_port is not None: + try: + port = int(server_port) + except (TypeError, ValueError): + raise ValueError('status-server.port must be an integer') + else: + port = 8090 return host, port From 07162527e01a4a0ef74520da2a75a8f59a60c7e0 Mon Sep 17 00:00:00 2001 From: randogoth Date: Fri, 19 Dec 2025 11:06:16 +0100 Subject: [PATCH 8/9] added link to original --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b12e70a..46d015d 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # App Timer (Parental Control) -Control apps usage time with a command line application. +Control apps usage time with a command line application. Forked from [macedd/app-timer](https://github.com/macedd/app-timer) The python daemon will watch for the apps executable and store usage time for them. It can block an app if the usage is off limits. From 5de3988c9995f7b256b5da151895c655257a8555 Mon Sep 17 00:00:00 2001 From: randogoth Date: Thu, 10 Sep 2026 09:56:09 +0200 Subject: [PATCH 9/9] Add migration notice --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index 46d015d..5931ad9 100644 --- a/readme.md +++ b/readme.md @@ -1,3 +1,5 @@ +> **Migrated to [code.randogoth.com/randogoth/app-timer](https://code.randogoth.com/randogoth/app-timer)** + # App Timer (Parental Control) Control apps usage time with a command line application. Forked from [macedd/app-timer](https://github.com/macedd/app-timer)