From ac149460ba743f5cca7e9a098e2e2b198e90c2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 21 Oct 2026 22:00:41 +0200 Subject: [PATCH] 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. --- .../blueprints/healthlog-room-climate.yaml | 28 +++--- docs/integrations/home-assistant.md | 39 ++++---- .../home-assistant-room-climate-docs.test.ts | 99 +++++++++++++++++++ 3 files changed, 136 insertions(+), 30 deletions(-) create mode 100644 src/__tests__/home-assistant-room-climate-docs.test.ts diff --git a/docs/integrations/blueprints/healthlog-room-climate.yaml b/docs/integrations/blueprints/healthlog-room-climate.yaml index 205ad0708..c573eb036 100644 --- a/docs/integrations/blueprints/healthlog-room-climate.yaml +++ b/docs/integrations/blueprints/healthlog-room-climate.yaml @@ -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 }}" diff --git a/docs/integrations/home-assistant.md b/docs/integrations/home-assistant.md index d5423e650..d858e7e80 100644 --- a/docs/integrations/home-assistant.md +++ b/docs/integrations/home-assistant.md @@ -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 diff --git a/src/__tests__/home-assistant-room-climate-docs.test.ts b/src/__tests__/home-assistant-room-climate-docs.test.ts new file mode 100644 index 000000000..58d583504 --- /dev/null +++ b/src/__tests__/home-assistant-room-climate-docs.test.ts @@ -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 { + return parse(readFileSync(BLUEPRINT, "utf8"), { + customTags: [inputTag as ScalarTag | CollectionTag], + }) as Record; +} + +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>; + const actions = doc.actions as Array>; + 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); + } + }); +});