mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(measurements): accept indoor CO2, temperature and humidity
Room climate from a home automation system gets three types of its own: INDOOR_CO2 (ppm), INDOOR_TEMPERATURE (celsius, shown in Fahrenheit for imperial readers) and INDOOR_HUMIDITY (percent). They arrive through the existing ingest token (measurements:write, source EXTERNAL), sit in the environment cluster beside daylight, and the daily rollup carries their mean, low and high like any spot reading. They never mix with the body temperatures or with the outdoor environment channels. The Home Assistant page documents the three types, a rest_command that sends a room as one request, a single-room automation, and an importable blueprint (docs/integrations/blueprints) that skips sensors reporting unknown or non-numeric states, converts Fahrenheit, and stamps readings to the minute so a retried push is refused rather than written twice.
This commit is contained in:
1 parent
b76826a5f8
commit
cc8ea3dfbb
32 files changed
+463
-9
No files matched your search
+30
-3
@@ -4321,6 +4321,9 @@ paths:
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- AFIB_BURDEN
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- in: query
|
||||
name: from
|
||||
schema:
|
||||
@@ -26000,7 +26003,7 @@ components:
|
||||
type: number
|
||||
const: 2
|
||||
leaves:
|
||||
maxItems: 101
|
||||
maxItems: 104
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -26060,7 +26063,7 @@ components:
|
||||
type: number
|
||||
const: 2
|
||||
leaves:
|
||||
maxItems: 101
|
||||
maxItems: 104
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -26901,6 +26904,9 @@ components:
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- AFIB_BURDEN
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
value:
|
||||
type: number
|
||||
measuredAt:
|
||||
@@ -28588,6 +28594,9 @@ components:
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- AFIB_BURDEN
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
biomarkerId:
|
||||
type: string
|
||||
minLength: 1
|
||||
@@ -29087,7 +29096,7 @@ components:
|
||||
type: number
|
||||
const: 2
|
||||
leaves:
|
||||
maxItems: 101
|
||||
maxItems: 104
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
@@ -30264,6 +30273,9 @@ components:
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- AFIB_BURDEN
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
description: DB-stored measurement category. v1.4.23 added 7 Apple Health values (HRV, resting HR, active energy,
|
||||
flights, walking/running distance, VO2 max, body temperature).
|
||||
InsightsSettingsPutRequest:
|
||||
@@ -39046,6 +39058,9 @@ components:
|
||||
- STAND_HOURS
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- AFIB_BURDEN
|
||||
value:
|
||||
type: number
|
||||
@@ -39792,6 +39807,9 @@ components:
|
||||
- STAND_HOURS
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- AFIB_BURDEN
|
||||
- type: "null"
|
||||
biomarkerName:
|
||||
@@ -42273,6 +42291,9 @@ components:
|
||||
- STAND_HOURS
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- AFIB_BURDEN
|
||||
dateKey:
|
||||
type: string
|
||||
@@ -43457,6 +43478,9 @@ components:
|
||||
- STAND_HOURS
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- AFIB_BURDEN
|
||||
- type: "null"
|
||||
intervalDays:
|
||||
@@ -43675,6 +43699,9 @@ components:
|
||||
- STAND_HOURS
|
||||
- MINDFUL_MINUTES
|
||||
- STATE_OF_MIND_VALENCE
|
||||
- INDOOR_CO2
|
||||
- INDOOR_TEMPERATURE
|
||||
- INDOOR_HUMIDITY
|
||||
- AFIB_BURDEN
|
||||
metricSlot:
|
||||
type:
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Push a room's CO2, temperature and humidity to HealthLog every few minutes.
|
||||
#
|
||||
# Needs the `healthlog_room_climate` rest_command from
|
||||
# docs/integrations/home-assistant.md ("Room climate") and a measurement
|
||||
# ingest token. Every sensor is optional; a sensor that reports `unknown`,
|
||||
# `unavailable` or a non-number is left out of that push, and a push with
|
||||
# nothing left is skipped. Temperatures in °F are converted to °C.
|
||||
blueprint:
|
||||
name: HealthLog room climate
|
||||
description: >-
|
||||
Sends indoor CO2 (ppm), temperature (°C) and relative humidity (%) to
|
||||
HealthLog through a measurement ingest token. HealthLog keeps each
|
||||
reading and shows the day's mean, low and high.
|
||||
domain: automation
|
||||
source_url: https://github.com/MBombeck/HealthLog/blob/main/docs/integrations/blueprints/healthlog-room-climate.yaml
|
||||
input:
|
||||
co2_sensor:
|
||||
name: CO2 sensor
|
||||
description: A sensor reporting carbon dioxide in ppm.
|
||||
default: ""
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
temperature_sensor:
|
||||
name: Temperature sensor
|
||||
description: A sensor reporting the room temperature (°C or °F).
|
||||
default: ""
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
device_class: temperature
|
||||
humidity_sensor:
|
||||
name: Humidity sensor
|
||||
description: A sensor reporting relative humidity in %.
|
||||
default: ""
|
||||
selector:
|
||||
entity:
|
||||
domain: sensor
|
||||
device_class: humidity
|
||||
interval_minutes:
|
||||
name: Interval
|
||||
description: Minutes between pushes. 15 is plenty for a daily picture.
|
||||
default: 15
|
||||
selector:
|
||||
number:
|
||||
min: 5
|
||||
max: 60
|
||||
step: 5
|
||||
unit_of_measurement: min
|
||||
|
||||
mode: single
|
||||
|
||||
variables:
|
||||
co2_sensor: !input co2_sensor
|
||||
temperature_sensor: !input temperature_sensor
|
||||
humidity_sensor: !input humidity_sensor
|
||||
interval_minutes: !input interval_minutes
|
||||
# A list of readings (Home Assistant keeps a rendered list a list).
|
||||
readings: >-
|
||||
{% set at = now().replace(second=0, microsecond=0).isoformat() %}
|
||||
{% set ns = namespace(items=[]) %}
|
||||
{% for type, entity in [
|
||||
('INDOOR_CO2', co2_sensor),
|
||||
('INDOOR_TEMPERATURE', temperature_sensor),
|
||||
('INDOOR_HUMIDITY', humidity_sensor)
|
||||
] %}
|
||||
{% if entity and is_number(states(entity)) %}
|
||||
{% set v = states(entity) | float %}
|
||||
{% if type == 'INDOOR_TEMPERATURE'
|
||||
and state_attr(entity, 'unit_of_measurement') == '°F' %}
|
||||
{% set v = ((v - 32) * 5 / 9) | round(2) %}
|
||||
{% endif %}
|
||||
{% set ns.items = ns.items
|
||||
+ [{'type': type, 'value': v, 'measuredAt': at}] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
minutes: "/5"
|
||||
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ now().minute % (interval_minutes | int(15)) == 0
|
||||
and readings | count > 0 }}
|
||||
|
||||
action:
|
||||
- service: rest_command.healthlog_room_climate
|
||||
data:
|
||||
readings: "{{ readings }}"
|
||||
@@ -149,6 +149,86 @@ Units are derived server-side from the type, so send the number in the unit
|
||||
above and do not send a `unit` field. The full enum is in
|
||||
`docs/api/openapi.yaml` under `MeasurementType`.
|
||||
|
||||
## Room climate
|
||||
|
||||
Indoor CO₂, temperature and humidity are their own types, kept apart from the
|
||||
body temperatures and from the outdoor weather the environment module pulls:
|
||||
|
||||
| What the sensor reports | `type` | Unit expected |
|
||||
| ----------------------- | -------------------- | ------------- |
|
||||
| Carbon dioxide | `INDOOR_CO2` | ppm |
|
||||
| Room temperature | `INDOOR_TEMPERATURE` | °C |
|
||||
| Relative humidity | `INDOOR_HUMIDITY` | % |
|
||||
|
||||
Each push is kept as a reading, and the day's mean, low and high are what the
|
||||
charts and the daily views read, so a sensor sampled every 15 minutes gives a
|
||||
clear picture without anything further on your side. Pushing every few seconds
|
||||
adds rows, not detail.
|
||||
|
||||
Add a second `rest_command` that takes a ready-made list of readings, so one
|
||||
request carries all three:
|
||||
|
||||
```yaml
|
||||
rest_command:
|
||||
healthlog_room_climate:
|
||||
url: "https://healthlog.example.com/api/measurements"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: !secret healthlog_token
|
||||
Content-Type: "application/json"
|
||||
payload: "{{ readings | to_json }}"
|
||||
```
|
||||
|
||||
Then import the blueprint, either from
|
||||
[`docs/integrations/blueprints/healthlog-room-climate.yaml`](./blueprints/healthlog-room-climate.yaml)
|
||||
or by its raw GitHub URL under Settings → Automations & scenes → Blueprints →
|
||||
Import blueprint, and create one automation per room. It asks for the three
|
||||
sensors (each optional) and an interval. Every push:
|
||||
|
||||
- leaves out a sensor that reports `unknown`, `unavailable` or anything that is
|
||||
not a number, and skips the push when nothing is left;
|
||||
- converts a temperature reported in °F to °C;
|
||||
- stamps all readings with the current minute, so a retry of the same push is
|
||||
refused with a 409 instead of written twice.
|
||||
|
||||
If you would rather write the automation yourself, this is the same thing for a
|
||||
single room without the blueprint:
|
||||
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Push bedroom climate to HealthLog"
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
minutes: "/15"
|
||||
variables:
|
||||
readings: >-
|
||||
{% set at = now().replace(second=0, microsecond=0).isoformat() %}
|
||||
{% set ns = namespace(items=[]) %}
|
||||
{% for type, entity in [
|
||||
('INDOOR_CO2', 'sensor.bedroom_co2'),
|
||||
('INDOOR_TEMPERATURE', 'sensor.bedroom_temperature'),
|
||||
('INDOOR_HUMIDITY', 'sensor.bedroom_humidity')
|
||||
] %}
|
||||
{% if is_number(states(entity)) %}
|
||||
{% set ns.items = ns.items + [{'type': type,
|
||||
'value': states(entity) | float, 'measuredAt': at}] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
condition:
|
||||
- condition: template
|
||||
value_template: "{{ readings | count > 0 }}"
|
||||
action:
|
||||
- service: rest_command.healthlog_room_climate
|
||||
data:
|
||||
readings: "{{ readings }}"
|
||||
```
|
||||
|
||||
One request takes up to five readings, so a room is one call. Several rooms
|
||||
are several automations: the readings carry no room name, and two rooms pushed
|
||||
in the same minute collide on the 409 above. If you track more than one room,
|
||||
pick the one you sleep in; that is the one worth reading next to your sleep.
|
||||
|
||||
## Sending several readings at once
|
||||
|
||||
A bare array writes up to five readings in one request — the shape to use for
|
||||
|
||||
Reference in new issue
Block a user