Thomas' writings, builds, toys

2020-12-04

Reviving my Standing stats project

concept

I've been using an Ergotron standing desk for 6+ years now. Several years ago I started building a sensor device to track how much time I spend standing up versus sitting down in my chair. The hardware prototype finished, it got shelved since I found other fun things to do with my limited hobby-time.

Recently I noticed that I sit down 99% of the time when working from home. To revive my healthy habit of working upright part of the time, I decided to pick up my parked project by re-printing the enclosure I made on my own printer and leveraging the power of esphome on a ESP8266 and home assistant to make the hardware useful.

desk

When I first started this project, I experimented a bit with sensors and found two easily available sensors that are suitable for the two distance measuring tasks; an ultrasonic distance sensor works fine for desk height, while an infrared triangulation distance sensor works better for the softer clothes worn on my torso (which don't reflect ultrasonic sound that well).

When I later got into 3D printing, the first design I made (and also most complex for years to come) was an enclosure that can be mounted on the standing desk.

Esphome brings all necessary tools to the table to easily interface the sensors with networked software; it uses platformio as a build environment, has drivers for a lot of sensors, integrates with home assistant easily and runs on ESP8266/ESP32 wifi chipsets. It was very easy to gather sensor statistics with such a setup.

The mechanics

I designed an enclosure that holds all electronics and mounts nicely and easily at an ideal spot of the desk's structure.

openscad render (with lots of artifacts)

I designed it with ease of printing and assembly in mind.

concept concept concept

The electronics and sensor software

I wired up a wemos D1 mini board with the sensors and neopixel LEDs.

substitutions:
  hostname: standingstats

esphome:
  name: ${hostname}
  platform: ESP8266
  board: nodemcuv2
  on_boot:
    priority: -10
    then:
      - light.turn_on:
          id: ${hostname}_kleuren
          brightness: 100%
wifi:
  ssid: !secret ssid
  password: !secret wlan
logger:
api:
  password: !secret api

ota:
  password: !secret ota

sensor:
  - platform: ultrasonic
    trigger_pin: D2
    echo_pin: D1
    name: "Desk Level"
    update_interval: 5s
  - platform: adc
    pin: A0
    name: "Presence"
    update_interval: 5s

light:
  - id: ${hostname}_kleuren
    name: ${hostname}_kleuren
    type: GRB
    platform: neopixelbus
    variant: WS2812
    method: BIT_BANG
    pin: D4
    num_leds: 5

Home assistant integration and business logic

With the current setup, no 'edge computing' is done, and all business logic is implemented using home assistant automations and sensors definitions.

home assistant view

The following sensor configuration provides a 'virtual' (template) sensor that determines the desk state as 'standing', 'sitting' and 'away' based on the sensor readings.

- platform: template
  sensors:
    desk:
      friendly_name: "desk"
      value_template: >-
        {% if float(states("sensor.presence")) < 0.1 -%}
          away
        {%- else -%}
        {% if float(states("sensor.desk_level")) > 0.2 -%}
          standing
        {%- else -%}
          sitting
        {%- endif %}
        {%- endif %}

The history_stats sensor platform provides quick statistics about another integration or platforms, using data from the history integration. I create two history_stats sensors that will sum up the time I spend sitting and standing for the current day.

- platform: history_stats
  name: Standing today
  entity_id: sensor.desk
  state: 'standing'
  type: time
  start: '{{ now().replace(hour=0, minute=0, second=0) }}'
  end: '{{ now() }}'

- platform: history_stats
  name: Sitting today
  entity_id: sensor.desk
  state: 'sitting'
  type: time
  start: '{{ now().replace(hour=0, minute=0, second=0) }}'
  end: '{{ now() }}'

I use an automation to have the color of neopixel LEDs reflect the desk state:

- alias: deskstatus
  trigger:
  - platform: state
    entity_id: sensor.desk
  action:
  - choose:
    - conditions:
        - condition: state
          entity_id: sensor.desk
          state: "sitting"
      sequence:
        - service: light.turn_on
          data:
            entity_id: light.standingstats_kleuren
            brightness: 255
            color_name: red
    - conditions:
        - condition: state
          entity_id: sensor.desk
          state: "standing"
      sequence:
        - service: light.turn_on
          data:
            entity_id: light.standingstats_kleuren
            brightness: 255
            color_name: green
    - conditions:
        - condition: state
          entity_id: sensor.desk
          state: "away"
      sequence:
        - service: light.turn_on
          data:
            entity_id: light.standingstats_kleuren
            brightness: 255
            color_name: blue

It's easy to calculate the ratio of standing versus sitting:

- platform: template
  name: standratio
  friendly_name: "Percentage standing"
  value_template: >-
    {{ (100*float(states("sensor.Standing_today"))/ (float(states("sensor.Standing_today")) + float(states("sensor.Sitting_today"))))|int }}
  unit_of_measurement: "%"

All I have to do now is create an automation that will notify me if I have been sitting down too long...

Status display

I have a hasp-lvgl switchplate on my desk, and I made a home assistant automation to show the sitting and standing time, along with an arc widget that shows the percentage I have been standing vs total time.

hasp lvgl screenshot

Screenshot of hasp-lvgl

The hasp lvgl jsonlines can probably be made simpler:

{"obj":"arc","id":29,"x":5,"y":200,"w":140,"h":100,"max":32767,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":-19,"value_ofs_y":-4,"bg_opa":0,"min":0}
{"obj":"label","id":31,"x":100,"y":210,"w":140,"h":30,"bg_color":"#000000","border_color":"#C7BAA7","border_width":0,"txt":"-","text_font":22}
{"obj":"label","id":32,"x":100,"y":240,"w":140,"h":30,"bg_color":"#000000","border_color":"#C7BAA7","border_width":0,"txt":"-","text_font":22}

The home assistant automation publishes updates to the widget's identifiers:

- alias: plate_display_standingtoday
  trigger:
  - platform: state
    entity_id: sensor.standing_today
  - platform: state
    entity_id: sensor.sitting_today
  - platform: state
    entity_id: sensor.standratio
  - platform: mqtt
    topic: "hasp/plate_c45378/LWT"
    payload: "online"
  action:
    - service: mqtt.publish
      data_template:
        topic: "hasp/plate_c45378/command/p1b29.val"
        payload: "{{ (states('sensor.standratio')|int )*327}}"
    - service: mqtt.publish
      data_template:
        topic: "hasp/plate_c45378/command/p1b31.txt"
        payload: "{{states('sensor.standing_today')|int}}h{{((float(states('sensor.standing_today'))%1)*60)|int}}m Standing"
    - service: mqtt.publish
      data_template:
        topic: "hasp/plate_c45378/command/p1b32.txt"
        payload: "{{states('sensor.sitting_today')|int}}h{{((float(states('sensor.sitting_today'))%1)*60)|int}}m Sitting"