Connecting a source
Home Assistant
The house already knows when you are at the desk. A motion sensor, a smart plug on the work machine, the office door - Home Assistant is holding all of it, and one small automation hands it over so the time records itself. Nothing to install, no add-on to trust: a few lines of YAML and a URL.
New here? Toiling Away turns recorded time into animals that live in a habitat of your own. The other half of Home Assistant here - a habitat on a wall tablet - is on the display page.
Make a ping URL
- In Toiling Away, open
Connections, addPing URL, and give it a label such ashome assistant. One URL per job, and the label is how you tell them apart later. - Copy the URL the moment it is shown. It is displayed once and stored hashed, so it cannot be recovered or shown again. Lose it and you make a new one.
That is the whole of the app-side setup. What these URLs can do in general is on the ping page; this page is about pointing Home Assistant at one.
The automation
Three verbs, all plain GETs: /start opens an interval, the plain URL closes it and records the elapsed time, /fail abandons an open one. The URL goes in secrets.yaml, which exists next to configuration.yaml for exactly this sort of thing:
# secrets.yaml
toil_start_url: https://app.toilingaway.com/p/YOUR-TOKEN/start
toil_close_url: https://app.toilingaway.com/p/YOUR-TOKEN
toil_fail_url: https://app.toilingaway.com/p/YOUR-TOKEN/fail
The three calls, in configuration.yaml. rest_command sends a GET when no method is named, which is what these want:
rest_command:
toil_start:
url: !secret toil_start_url
toil_close:
url: !secret toil_close_url
toil_fail:
url: !secret toil_fail_url
And the automation, in automations.yaml, with binary_sensor.desk_occupancy replaced by your own sensor:
- alias: Desk time into Toiling Away
trigger:
- platform: state
entity_id: binary_sensor.desk_occupancy
to: "on"
id: sat_down
- platform: state
entity_id: binary_sensor.desk_occupancy
to: "off"
for: "00:05:00"
id: got_up
- platform: homeassistant
event: start
id: restarted
action:
- choose:
- conditions:
- condition: trigger
id: restarted
sequence:
- service: rest_command.toil_fail
- if:
- condition: state
entity_id: binary_sensor.desk_occupancy
state: "on"
then:
- service: rest_command.toil_start
- conditions:
- condition: trigger
id: sat_down
sequence:
- service: rest_command.toil_start
- conditions:
- condition: trigger
id: got_up
sequence:
- service: rest_command.toil_close
Why this shape and not a simpler one:
The automation never states a duration. It opens the interval and closes it, and the server timestamps both ends, so a run cannot claim more time than actually elapsed. The ping URL also accepts a seconds= parameter that states the length outright, and an automation has no business sending it: a number computed inside Home Assistant is a number nobody checked. It would be ignored anyway while an interval is open.
A second start does nothing. The server only opens an interval when none is open, so a sensor that flicks on twice opens nothing new. Start, close, start is three calls and two intervals; start, start, close is three calls and one.
A restart abandons the run. The third trigger fires when Home Assistant boots and calls /fail, so the run the restart left open is dropped instead of sitting there waiting to be credited as four hours. The if after it opens a fresh one if the desk is occupied right now.
Each branch asks which trigger fired, not what the sensor says now. That is the whole reason for the id: on each trigger. Branch on the sensor's current state instead and the restart never reaches /fail: at boot the sensor reads on or off like any other moment, so the restart falls into whichever ordinary branch matches and the stale run is never dropped. Worse, the branch it lands in on an empty desk is the close, and a close with nothing open is not nothing - see below.
Why the off transition waits five minutes
Presence sensors flap. A motion sensor declares the room empty after a few minutes of stillness, and door sensors chatter as people come and go. An automation that starts on on and closes on off with nothing in between turns one afternoon into dozens of two-minute intervals. The for: holds the off transition for five minutes before the close fires, so a dropout shorter than that never closes anything.
Pick the delay with the sensor in mind: longer than the sensor's own idle timeout, and long enough to cover a refill of the coffee. The cost is one-sided and worth knowing: a stretch away from the desk shorter than the delay does not get counted.
One ping per state change, not one per sensor report. Each state change is two calls, and the URL takes sixty requests a minute and no more - counted on the token, not your address - after which the server refuses the rest of the minute with a 429. That is harder to trip than it sounds, but only because the triggers above fire when the state flips, not when the sensor reports. If your signal is a number - watts drawn, say - do not trigger on the number itself, which changes with every reading. Wrap it in a template binary sensor, give that one a for: on each side, and the automation fires when the machine has settled into on or off.
The ceilings, plainly
Four hours to a run. A run left open past four hours is credited as four, not as the whole absence. A sensor stuck on over a weekend is a four-hour credit, which is what the restart trigger above is for rather than a setting to change.
Twelve hours a day to the connection. Past that, the close answers 422, Daily ping limit exceeded., and the run it was closing is abandoned rather than half-credited. Nothing in the automation fixes it. Note the stretch down and type it through Log time in the app if the day still has room for it, or let it go. If this happens on an ordinary working day, the ceiling on the account is the thing to fix, and that is a change on the server rather than a setting here.
Overlaps merge. If Home Assistant and a timesheet cover the same hour, it counts once. You cannot earn twice from one hour by tracking it twice.
What the sensor saw, and what it means
The server counts what the sensor saw; only you know what it meant. Motion says somebody was in the room, and whether that was work is the part the signal has to carry. It is worth choosing the sensor accordingly, because a habitat that grew from hours near a desk is a less interesting thing than one that grew from hours of work.
A signal with an intent behind it:
- A smart plug on the work machine. Power drawn means somebody decided to work, and the automation above drops straight onto a template binary sensor built from it.
- An office door, with a condition. Shut during working hours is an office; shut at midnight is a closed door. A time condition on the automation tells those apart.
- A button. You saying so, at the moment you sit down. Nothing is more honest, and it is the same three verbs.
Bare motion is the weakest of the three. It records an afternoon of reorganising the garage with exactly the confidence it records a deadline, and it is the version to pick only when the room is a work room and nothing else.
Keep the URL to yourself
Anybody holding it can record work as you; it is the whole of the authentication, which is what makes it usable from an automation with no login anywhere. In Home Assistant terms that means secrets.yaml and nowhere else: not in configuration.yaml, not in an exported automation, not in a shared dashboard or a screenshot. If it does get out, delete the connection and make another; the old URL stops working immediately.
What it looks like when it misfires
Every call is written down before the server decides anything about it, so a refused ping leaves a row the same as a credited one. Open Connections, choose the connection, and the recent pings list shows the last twenty: which action, from where, when. It will not say whether toil was credited - only what arrived, which is usually the question.
- A start, then several more starts. Only the first start of a run does anything, so this is the debounce missing: the interval opened once and everything after it was noise.
- A start with no close under it. The interval never closed: Home Assistant restarted without the fail trigger, or the off transition never held for the whole delay.
- A close that is not followed by new time. The day was already over its ceiling. The Home Assistant log shows the same call as an error, because
rest_commandrecords anything but a success as one, so a refused close is visible from both ends. - A close with no start before it. Once the connection has closed its first real interval, the plain URL means only that: a close with nothing open is written down and credits nothing, so the retry a dropped response provokes cannot be paid for twice. Before that first interval it is still a bare ping, and a bare ping counts as one thing done, so a stray close on a connection that has never timed anything can credit work that did not happen. Either way the row is there to see, and something calling the close outside the pair is worth fixing - the usual cause is a branch that runs on a restart.
- An address that is not yours. The URL is out in the world. Delete the connection, make a new one, and put the new one in
secrets.yaml.