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.)

1 Like

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.

1 Like

So, I looked into this again, but I don’t think what you mentioned works.

It probably works for your scenario, but what I’m asking in this thread is how to distinguish between two different FocusMate sessions. As far as I know, this is currently impossible because there’s no way to “tag” a session with some unique ID or a string as the doc also mentions.

1 Like

Thanks for this script. From what I understand, this is what I need to do currently to differentiate different FocusMate sessions. The “User” object gives you the title of the session, so as long as I use the same title for specific tasks, I can code everything from that.

On the Beeminder side, I can set up a meta minder which accepts all FocusMate sessions. Then, when a new datapoint is added, I can invoke the script which will post a new dataset to the specific Beeminder goal. That’s how I understand this.

Yes, for whatever reason Focusmate session objects don’t have a title. However, there is a list of users where the first user is yourself and has a sessionTitle attribute. That’s the same comment you can put into a session via the pencil symbol on the Focusmate app.

In my case, I have a main /focusmate goal, and the script populates that goal with all completed sessions. The reason for that is that the Beeminder integration with Focusmate synchronizes uncompleted session which leads to the number of sessions on Beeminder and Focusmate being out of sync which I don’t like.

However, the main /focusmate goal is never read, so it’s not a meta goal in the sense that Beeminder uses the term. Here is the data flow that the script implements, just to be clear:

1 Like

Thanks. I thought about it a bit more, and I think I can use make.com to automate all of this, so I don’t need to write any scripts or run it manually or schedule it to run.

  1. Set up multiple Beeminder goals using FocusMate.
  2. Complete a FocusMate session.
  3. All Beeminder goals tied to FocusMate gets updated. (Really stupid, but that’s how it is today.)
  4. Create a new scenario at make.com which uses the Beeminder web hook on a datapoint creation and delete the latest datapoint if the session title does not match the one specific to each Beeminder goal.

I have not tested this, but it should work in theory.

2 Likes

Except that, as you pointed out to me, the session title doesn’t currently appear in the Beeminder datapoint comment. If it did, then you could set up a single Focusmate goal with a webhook to make.com, and add other datapoints based on what’s in the comment.

But I can imagine using that webhook to trigger checking the Focusmate API to get richer data about the session you just had with the other user, and then creating datapoints on the relevant Beeminder goals.