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

  1. In Toiling Away, open Connections, add Ping URL, and give it a label such as home assistant. One URL per job, and the label is how you tell them apart later.
  2. 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_command records 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.