Skip to content

Commit

Permalink
feat: misc improvements, added hourly command
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Aug 3, 2023
1 parent 139afac commit d9e2e28
Showing 1 changed file with 90 additions and 16 deletions.
106 changes: 90 additions & 16 deletions aw_notify/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
and send notifications to the user on predefined conditions.
"""

import logging
import threading
from datetime import datetime, timedelta, timezone
from time import sleep

Expand All @@ -11,6 +13,8 @@
import click
from desktop_notifier import DesktopNotifier

logger = logging.getLogger(__name__)

# TODO: Get categories from aw-webui export (in the future from server key-val store)
# TODO: Add thresholds for total time today (incl percentage of productive time)

Expand Down Expand Up @@ -39,10 +43,10 @@

time_offset = timedelta(hours=4)

aw = aw_client.ActivityWatchClient("aw-notify", testing=False)

def get_time(category: str) -> timedelta:
aw = aw_client.ActivityWatchClient("aw-notify", testing=False)

def get_time(category: str) -> timedelta:
now = datetime.now(timezone.utc)
timeperiods = [
(
Expand All @@ -51,10 +55,11 @@ def get_time(category: str) -> timedelta:
)
]

hostname = aw.get_info().get("hostname", "unknown")
canonicalQuery = aw_client.queries.canonicalEvents(
aw_client.queries.DesktopQueryParams(
bid_window="aw-watcher-window_erb-m2.localdomain",
bid_afk="aw-watcher-afk_erb-m2.localdomain",
bid_window=f"aw-watcher-window_{hostname}",
bid_afk=f"aw-watcher-afk_{hostname}",
classes=CATEGORIES,
filter_classes=[[category]] if category else [],
)
Expand Down Expand Up @@ -89,19 +94,19 @@ def to_hms(duration: timedelta) -> str:
notifier: DesktopNotifier = None


def notify(msg: str):
def notify(title: str, msg: str):
# send a notification to the user

global notifier
if notifier is None:
notifier = DesktopNotifier(
app_name="ActivityWatch notify",
app_name="ActivityWatch",
# icon="file:///path/to/icon.png",
notification_limit=10,
)

print(msg)
notifier.send_sync(title="AW", message=msg)
notifier.send_sync(title=title, message=msg)


td15min = timedelta(minutes=15)
Expand Down Expand Up @@ -153,21 +158,24 @@ def update(self):
time_to_threshold = self.time_to_next_threshold
# print("Update?")
if now > (self.last_check + time_to_threshold):
print(f"Updating {self.category}")
logger.debug(f"Updating {self.category}")
# print(f"Time to threshold: {time_to_threshold}")
self.last_check = now
self.time_spent = get_time(self.category)
else:
pass
# print("Not updating, too soon")
# logger.debug("Not updating, too soon")

def check(self):
"""Check if thresholds have been reached"""
for thres in sorted(self.thresholds_untriggered, reverse=True):
if thres <= self.time_spent:
# threshold reached
self.max_triggered = thres
notify(f"[Alert]: {self.category or 'All'} for {to_hms(thres)}")
notify(
"Time spent",
f"{self.category or 'All'}: {to_hms(self.time_spent)}",
)
break

def status(self) -> str:
Expand All @@ -183,14 +191,17 @@ def test_category_alert():


@click.group()
def main():
pass
@click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.")
def main(verbose: bool):
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO)
logging.getLogger("urllib3").setLevel(logging.WARNING)


@main.command()
def start():
"""Start the notification service."""
checkin()
hourly()
threshold_alerts()


Expand All @@ -208,24 +219,87 @@ def threshold_alerts():
for alert in alerts:
alert.update()
alert.check()
print(alert.status())
status = alert.status()
if status != getattr(alert, "last_status", None):
print(f"New status: {status}")
alert.last_status = status

sleep(10)


@main.command()
def _checkin():
"""Send a summary notification."""
checkin()


def checkin():
"""
Sends a summary notification of the day.
Meant to be sent at a particular time, like at the end of a working day (e.g. 5pm).
"""
# TODO: load categories from data
top_categories = ["", "Work", "Twitter"]
top_categories = [""] + [k[0] for k, _ in CATEGORIES]
time_spent = [get_time(c) for c in top_categories]
msg = f"Time spent today: {sum(time_spent, timedelta())}\n"
msg += "Categories:\n"
msg += "\n".join(f" - {c}: {t}" for c, t in zip(top_categories, time_spent))
notify(msg)
msg += "\n".join(
f" - {c if c else 'All'}: {t}"
for c, t in sorted(
zip(top_categories, time_spent), key=lambda x: x[1], reverse=True
)
)
notify("Checkin", msg)


def get_active_status() -> bool:
"""
Get active status by polling latest event in aw-watcher-afk bucket.
Returns True if user is active/not-afk, False if not.
On error, like out-of-date event, returns None.
"""

hostname = aw.get_info().get("hostname", "unknown")
events = aw.get_events(f"aw-watcher-afk_{hostname}", limit=1)
print(events)
if not events:
return None
event = events[0]
if event.timestamp < datetime.now(timezone.utc) - timedelta(minutes=5):
# event is too old
logger.warning(
"AFK event is too old, can't use to reliably determine AFK state"
)
return None
return events[0]["data"]["status"] == "not-afk"


def hourly():
"""Start a thread that does hourly checkins, on every whole hour that the user is active (not if afk)."""

def checkin_thread():
while True:
# wait until next whole hour
now = datetime.now(timezone.utc)
next_hour = now.replace(minute=0, second=0, microsecond=0) + timedelta(
hours=1
)
sleep_time = (next_hour - now).total_seconds()
logger.debug(f"Sleeping for {sleep_time} seconds")
sleep(sleep_time)

# check if user is afk
active = get_active_status()
if active is None:
logger.warning("Can't determine AFK status, skipping hourly checkin")
continue
if not active:
logger.info("User is AFK, skipping hourly checkin")
continue

checkin()

threading.Thread(target=checkin_thread, daemon=True).start()


if __name__ == "__main__":
Expand Down

0 comments on commit d9e2e28

Please sign in to comment.