Is it possible to create multiple goals from the same source?

I just started using Beeminder with FocusMate, and I want to set up two different goals (learning language and practicing instrument) using FocusMate. When I completed a FocusMate session, it updated both of my Beeminder goals. Is there a workaround for this?

2 Likes

Looks like this is not possible yet acconding to this. Focusmate - Beeminder Help

Can I track sessions that are only about a specific thing?
Not yet! Focusmate doesn’t yet have a method available for us to check the title of the session, or a method of tagging sessions, but we hope this feature can be added in future.

But, the same help mentions they can track the length of a session, so if your goals have different session length, we colud consider using that. In my case, my goals have the same session length.

1 Like

How familiar are you with programming, or more concretely, do you know how to run a Python script?

I have a similar use-case where I have one main /focusmate goal, but then I use tags like #work, #euler, #highlight in the Focusmate session comment to add the same data point to these specific goals.

If you are interested, I could polish the script a little and share it on Github or something.

1 Like

Hi! I’m a software engineer, so I understand code. If it’s a script, don’t I need a service running somewhere that triggers it? And yes, it’d be nice if you could share the script. I prefer JavaScript, but any language is fine.

2 Likes

Yes, embarrassingly I just run it once a day manually during my evening routine.

Ah, okay. I just paste the unpolished thing below as inspiration and you can put it into GPT to turn it into JavaScript. Sorry.

#!/usr/bin/env python3

import requests
import keyring
import sys
from datetime import datetime, timedelta
from pydantic import BaseModel
from typing import List, Optional


BEEMINDER_AUTH_TOKEN = ""
FOCUSMATE_API_KEY = ""
FOCUSMATE_BASE_URL = "https://api.focusmate.com/v1"
USER = "user"
MAIN_GOAL = "focusmate"
OTHER_GOALS = ["work", "highlight"]
BEEMINDER_URL = "https://www.beeminder.com/api/v1/users/{user}/goals/{goal}/datapoints.json"


class User(BaseModel):
    userId: str
    requestedAt: Optional[str] = None
    joinedAt: Optional[str] = None
    completed: Optional[bool] = None
    sessionTitle: Optional[str] = None


class Session(BaseModel):
    sessionId: str
    duration: int
    startTime: str
    users: List[User]

    def get_timestamp(self) -> int:
        return int(datetime.fromisoformat(self.startTime).timestamp())

    def get_user_name(self) -> Optional[str]:
        if len(self.users) > 1:
            user_id = self.users[1].userId
            return get_user_name(user_id)
        else:
            return "No partner"


class BeeminderDatapoint(BaseModel):
    id: str
    timestamp: int
    daystamp: str
    value: int
    comment: str
    updated_at: int
    requestid: Optional[str]

    def get_timestamp(self) -> int:
        return self.timestamp


def get_user_name(user_id: str) -> Optional[str]:
        response = requests.get(
            f"{FOCUSMATE_BASE_URL}/users/{user_id}",
            headers={"X-API-KEY": FOCUSMATE_API_KEY}
        )

        if response.status_code != 200:
            print(f"Unable to fetch user data: {response.status_code}")
            return None

        data = response.json()
        return data['user']['name']


def get_sessions(start_date: str, end_date: str) -> List[Session]:
    response = requests.get(
        f"{FOCUSMATE_BASE_URL}/sessions?start={start_date}&end={end_date}",
        headers={"X-API-KEY": FOCUSMATE_API_KEY}
    )

    if response.status_code != 200:
        print(f"Unable to fetch session data. Status code: {response.status_code}")
        return []

    data = response.json()
    sessions = [Session.model_validate(session) for session in data['sessions']]
    return sessions


def get_sessions_from(latest_timestamp: Optional[datetime]) -> List[Session]:
    sessions = []
    if latest_timestamp is None:
        latest_timestamp = datetime(2015, 1, 1)
    current_timestamp = datetime.now()
    while latest_timestamp < current_timestamp:
        start_date = latest_timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
        end_date = (latest_timestamp + timedelta(days=365)).strftime("%Y-%m-%dT%H:%M:%SZ")
        sessions += get_sessions(start_date, end_date)
        latest_timestamp += timedelta(days=365)
    sessions.sort(key=lambda s: datetime.fromisoformat(s.startTime))
    return sessions


def post_to_beeminder(session: Session):
    # Comment
    partner_name = session.get_user_name()
    session_title = session.users[0].sessionTitle
    session_start_time = datetime.fromisoformat(session.startTime)
    start_day = session_start_time.strftime("%a") # format start time as day of the week
    duration_mins = session.duration // 60000  # milliseconds to minutes
    comment = f"{start_day}, {duration_mins} mins, {partner_name}"
    if session_title:
        comment += f", {session_title}"

    if not session.users[0].completed:
        print(f"Did not post uncompleted session '{comment}'.")
        return None

    if not session_title:
        print(f"Did not post completed session '{comment}' because it does not have a title.")
        sys.exit(1)

    # Timestamp
    timestamp = session.get_timestamp()
    daystamp = session_start_time.strftime("%Y%m%d")
    data = {
        "auth_token": BEEMINDER_AUTH_TOKEN,
        "value": 1,
        "comment": comment,
        "timestamp": timestamp,
        "daystamp": daystamp
    }

    # Post
    response = requests.post(BEEMINDER_URL.format(user=USER, goal=MAIN_GOAL), data=data)
    if response.status_code != 200:
        print(f"Unable to post to Beeminder: {response.status_code}")
        return None
    else:
        m = f"Posted '{comment}' to"
        for other_goal in OTHER_GOALS:
            if f"#{other_goal}" in session_title:
                response = requests.post(BEEMINDER_URL.format(user=USER, goal=other_goal), data=data)
                m += f" /{other_goal}"
        m += " /focusmate."
        print(m)


def get_beeminder_datapoints(count: Optional[int]) -> List[BeeminderDatapoint]:
    params = {
        "auth_token": BEEMINDER_AUTH_TOKEN,
        "sort": "timestamp"
    }
    if count is not None:
        params['count'] = count

    response = requests.get(BEEMINDER_URL.format(user=USER, goal=MAIN_GOAL), params=params)
    if response.status_code != 200:
        print(f"Unable to fetch datapoint from Beeminder. Status code: {response.status_code}")
        return []

    data = response.json()
    return [BeeminderDatapoint.model_validate(o) for o in data]


def update():
    latest_datapoints = get_beeminder_datapoints(count=1)
    if latest_datapoints == [] or latest_datapoints[0].value == 0:
        latest_timestamp = datetime(2015, 1, 1)
    else:
        # Convert timestamp to datetime object
        latest_timestamp = datetime.fromtimestamp(latest_datapoints[0].timestamp)

    sessions = get_sessions_from(latest_timestamp)

    # Get existing datapoints to avoid posting the same session twice
    datapoints = get_beeminder_datapoints(count=len(sessions))
    existing_timestamps = [d.timestamp for d in datapoints]

    for session in sessions:
        if session.get_timestamp() not in existing_timestamps:
            post_to_beeminder(session)


if __name__ == "__main__":
    update()
2 Likes

I use make.com to run my Beeminder script filters, it works pretty well.

You can set up a Make webhook and add it to any Beeminder goal on the settings page, which will then send a JSON notification every time a datapoint is added to that goal. There’s a Beeminder module for Make that lets you easily add datapoints to other goals.

I use this for my own Focusmate goals, to add the appropriate amount of pomodoro time to Exist.io

Previously, I’ve used IFTTT and Zapier to achieve similar things.

2 Likes

This sounds like a vote for our overdue extension of the Beeminder Metaminder integration to allow echoing datapoints from one goal to another. (Currently Metaminder only can do +1’s to a child goal when data is entered on a parent goal.)

Are we talking about the same scenario? If I use one FocusMate session for a Beeminder goal and a different FocusMate session for a different Beeminder goal, how do you differentiate these two sessions and direct FocusMate to make a call to its corresponding Beeminder goal?

To do this, I need to differentiate between different FocusMate sessions, and hook into their API somehow.