mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
docs(home-assistant): one room, intervals that divide the hour, current keys
The room-climate guide said both to create one automation per room and to track only one; it now recommends the bedroom and says why a second room cannot work (no room name, same-minute pushes collide on 409). The blueprint offers 5, 10, 15, 20, 30 or 60 minutes, so the push lands on the stated minutes, and it and the guide's examples use the triggers, conditions and actions keys of Home Assistant 2024.10 and later. A test holds all three.
This commit is contained in:
1 parent
e8f34315d4
commit
ac149460ba
3 files changed
+136
-30
No files matched your search
@@ -1,5 +1,8 @@
|
||||
# Push a room's CO2, temperature and humidity to HealthLog every few minutes.
|
||||
#
|
||||
# Use it for one room. The readings carry no room name, so a second room would
|
||||
# land in the same series, and a push in the same minute is refused (409).
|
||||
#
|
||||
# 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`,
|
||||
@@ -12,6 +15,8 @@ blueprint:
|
||||
HealthLog through a measurement ingest token. HealthLog keeps each
|
||||
reading and shows the day's mean, low and high.
|
||||
domain: automation
|
||||
homeassistant:
|
||||
min_version: 2024.10.0
|
||||
source_url: https://github.com/MBombeck/HealthLog/blob/main/docs/integrations/blueprints/healthlog-room-climate.yaml
|
||||
input:
|
||||
co2_sensor:
|
||||
@@ -40,14 +45,13 @@ blueprint:
|
||||
device_class: humidity
|
||||
interval_minutes:
|
||||
name: Interval
|
||||
description: Minutes between pushes. 15 is plenty for a daily picture.
|
||||
default: 15
|
||||
description: >-
|
||||
Minutes between pushes, on the hour and every interval after it.
|
||||
15 is plenty for a daily picture.
|
||||
default: "15"
|
||||
selector:
|
||||
number:
|
||||
min: 5
|
||||
max: 60
|
||||
step: 5
|
||||
unit_of_measurement: min
|
||||
select:
|
||||
options: ["5", "10", "15", "20", "30", "60"]
|
||||
|
||||
mode: single
|
||||
|
||||
@@ -77,17 +81,17 @@ variables:
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/5"
|
||||
|
||||
condition:
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: >-
|
||||
{{ now().minute % (interval_minutes | int(15)) == 0
|
||||
and readings | count > 0 }}
|
||||
|
||||
action:
|
||||
- service: rest_command.healthlog_room_climate
|
||||
actions:
|
||||
- action: rest_command.healthlog_room_climate
|
||||
data:
|
||||
readings: "{{ readings }}"
|
||||
@@ -109,20 +109,20 @@ a new weight:
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Push weight to HealthLog"
|
||||
trigger:
|
||||
- platform: state
|
||||
triggers:
|
||||
- trigger: state
|
||||
entity_id: sensor.bathroom_scale_weight
|
||||
condition:
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ trigger.to_state.state not in ['unknown', 'unavailable'] }}"
|
||||
action:
|
||||
- service: rest_command.healthlog_measurement
|
||||
actions:
|
||||
- action: rest_command.healthlog_measurement
|
||||
data:
|
||||
type: WEIGHT
|
||||
value: "{{ trigger.to_state.state | float }}"
|
||||
```
|
||||
|
||||
The `condition` is not decoration. Home Assistant sensors publish `unknown` and
|
||||
The condition is not decoration. Home Assistant sensors publish `unknown` and
|
||||
`unavailable` as ordinary states, and forwarding one produces a 422 on every
|
||||
restart.
|
||||
|
||||
@@ -179,11 +179,17 @@ rest_command:
|
||||
payload: "{{ readings | to_json }}"
|
||||
```
|
||||
|
||||
Then import the blueprint, either from
|
||||
Track one room, the one you sleep in: that is the one worth reading next to
|
||||
your sleep. The readings carry no room name, so a second room would land in the
|
||||
same series and blur both, and two pushes in the same minute collide on a 409
|
||||
(see below).
|
||||
|
||||
Then import the blueprint (Home Assistant 2024.10 or newer), 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:
|
||||
Import blueprint, and create one automation from it for that room. It asks for
|
||||
the three sensors (each optional) and an interval of 5, 10, 15, 20, 30 or 60
|
||||
minutes, counted from the full hour. 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;
|
||||
@@ -197,8 +203,8 @@ single room without the blueprint:
|
||||
```yaml
|
||||
automation:
|
||||
- alias: "Push bedroom climate to HealthLog"
|
||||
trigger:
|
||||
- platform: time_pattern
|
||||
triggers:
|
||||
- trigger: time_pattern
|
||||
minutes: "/15"
|
||||
variables:
|
||||
readings: >-
|
||||
@@ -215,19 +221,16 @@ automation:
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ ns.items }}
|
||||
condition:
|
||||
conditions:
|
||||
- condition: template
|
||||
value_template: "{{ readings | count > 0 }}"
|
||||
action:
|
||||
- service: rest_command.healthlog_room_climate
|
||||
actions:
|
||||
- action: 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.
|
||||
One request takes up to five readings, so the room is one call.
|
||||
|
||||
## Sending several readings at once
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* The room-climate blueprint and its guide say one consistent thing.
|
||||
*
|
||||
* - The blueprint uses Home Assistant's current automation keys (`triggers`,
|
||||
* `conditions`, `actions`, `trigger:` and `action:` inside them) and says
|
||||
* which release it needs for them.
|
||||
* - Every interval it offers divides the hour, so "every N minutes from the
|
||||
* full hour" is what actually happens: the trigger fires every five minutes
|
||||
* and the condition keeps `minute % N == 0`, which for an N that does not
|
||||
* divide 60 drifts at the hour.
|
||||
* - The guide recommends one room and never tells a reader to create one
|
||||
* automation per room, which the readings (no room name) cannot support.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parse, Scalar, type CollectionTag, type ScalarTag } from "yaml";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const BLUEPRINT = join(
|
||||
ROOT,
|
||||
"docs/integrations/blueprints/healthlog-room-climate.yaml",
|
||||
);
|
||||
const GUIDE = join(ROOT, "docs/integrations/home-assistant.md");
|
||||
|
||||
const inputTag: ScalarTag = {
|
||||
tag: "!input",
|
||||
resolve: (value: string) => new Scalar(`!input ${value}`),
|
||||
};
|
||||
|
||||
function blueprint(): Record<string, unknown> {
|
||||
return parse(readFileSync(BLUEPRINT, "utf8"), {
|
||||
customTags: [inputTag as ScalarTag | CollectionTag],
|
||||
}) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("room-climate blueprint", () => {
|
||||
it("uses the current automation keys and names the release they need", () => {
|
||||
const doc = blueprint();
|
||||
for (const legacy of ["trigger", "condition", "action"]) {
|
||||
expect(doc, legacy).not.toHaveProperty(legacy);
|
||||
}
|
||||
const triggers = doc.triggers as Array<Record<string, unknown>>;
|
||||
const actions = doc.actions as Array<Record<string, unknown>>;
|
||||
expect(triggers.every((t) => "trigger" in t && !("platform" in t))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(actions.every((a) => "action" in a && !("service" in a))).toBe(true);
|
||||
const meta = doc.blueprint as {
|
||||
homeassistant?: { min_version?: string };
|
||||
};
|
||||
expect(meta.homeassistant?.min_version).toMatch(/^2024\.10\./);
|
||||
});
|
||||
|
||||
it("offers only intervals that divide the hour", () => {
|
||||
const meta = blueprint().blueprint as {
|
||||
input: {
|
||||
interval_minutes: {
|
||||
default: string;
|
||||
selector: { select: { options: string[] } };
|
||||
};
|
||||
};
|
||||
};
|
||||
const options = meta.input.interval_minutes.selector.select.options;
|
||||
expect(options.length).toBeGreaterThan(0);
|
||||
for (const option of options) {
|
||||
expect(60 % Number(option), option).toBe(0);
|
||||
}
|
||||
expect(options).toContain(meta.input.interval_minutes.default);
|
||||
});
|
||||
});
|
||||
|
||||
describe("room-climate guide", () => {
|
||||
const guide = readFileSync(GUIDE, "utf8");
|
||||
const section = guide.slice(
|
||||
guide.indexOf("## Room climate"),
|
||||
guide.indexOf("## Sending several readings at once"),
|
||||
);
|
||||
|
||||
it("recommends one room and explains why", () => {
|
||||
expect(section.length).toBeGreaterThan(200);
|
||||
expect(section).toMatch(/Track one room/);
|
||||
expect(section).toMatch(/409/);
|
||||
expect(section).not.toMatch(/one automation per room/i);
|
||||
expect(section).not.toMatch(/several automations/i);
|
||||
});
|
||||
|
||||
it("writes its examples with the current keys", () => {
|
||||
const examples = [...guide.matchAll(/```yaml\n([\s\S]*?)```/g)].map(
|
||||
(m) => m[1]!,
|
||||
);
|
||||
expect(examples.length).toBeGreaterThan(0);
|
||||
for (const example of examples) {
|
||||
expect(example).not.toMatch(/^\s+- platform:/m);
|
||||
expect(example).not.toMatch(/^\s+- service:/m);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user