Home Assistant for Meatbags
  • Jinja 76.5%
  • Just 23.5%
Find a file
Claudia 3a493d8127
docs: plan for house ambient RH off a real sensor
The winter ambient figure is 35 % and has never been measured — its own
comment in helpers/input_number.yaml says so — and every winter drying decision
in three bathrooms rests on it. The summer figure was measured off the Sector
Alarm smoke detectors, which are a closed system HA cannot read, so repeating
the measurement means going and reading a panel by hand.

A sensor on the floor 2 landing removes both the guess and the two-season
approximation of a quantity that moves daily. The engage and release offsets
carry over untouched, because they were always expressed as an excess over
ambient rather than as absolute humidity. It also leaves summer_mode with only
the heating branch to do, which is the clean boundary for retiring it.

Recorded as a plan rather than written as config because the sensor does not
exist yet, and CLAUDE.md rule 2 is to verify entity IDs rather than guess at
them. docs/ is outside the just sync rsync list, so it stays in the repo.
2026-08-11 21:00:15 +02:00
automations feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
blueprints kari: update configuration 2026-01-21 21:32:36 +01:00
custom_templates feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
dashboards feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
docs docs: plan for house ambient RH off a real sensor 2026-08-11 21:00:15 +02:00
helpers feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
sensors feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
templates feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
.gitignore chore: update gitignore 2026-08-10 23:30:32 +02:00
.sops.yaml feat: secrets encrypted at rest with sops/age 2026-08-11 11:19:30 +02:00
CLAUDE.md chore: price alignments 2026-03-15 08:11:53 +01:00
configuration.yaml feat: seasons detected from the weather, not flipped by hand 2026-08-11 21:00:09 +02:00
customize.yaml kari: update configuration 2026-01-21 21:32:36 +01:00
justfile fix: reach vitals on the LAN, and say 'no reading' in a state HA accepts 2026-08-11 11:30:47 +02:00
README.md fix: a failed decrypt no longer wipes the secrets on the box 2026-08-11 11:23:03 +02:00
secrets.sops.yaml feat: secrets encrypted at rest with sops/age 2026-08-11 11:19:30 +02:00

Home Assistant

Contains custom stuff for HASS.

Accurate appliance running cost

Price-weighted accumulated cost for a switched appliance on a metered plug. Each moment's cost uses the all-in price (sensor.nordpool_price_with_tariffs = spot + energiskatt + elöverföring + markup, ×1.25 VAT) in effect then, so daily/monthly totals reflect when the appliance actually ran — unlike total_kWh × current_price, which drifts with the current hour.

All costs are consolidated on the Energy Costs dashboard view, grouped by category. Most loads live in templates/energy_costs.yaml + sensors/energy_costs.yaml; dehumidifier and hot water keep their pipeline in their own domain files but feed the same view. Category tables are rendered by the cost_rows macro (also in custom_templates/energy_cost.jinja), which takes [(label, daily_entity, monthly_entity), …], sorts by today's cost, and appends a bold Subtotal row.

To add another appliance with a sensor.<x>_power sensor, replicate these three primitives (then add a row to the relevant category card, and just restart — new platform sensors don't load on reload_all):

  1. Cost-rate template (templates/energy_costs.yaml) — SEK/h, via the shared macro in custom_templates/energy_cost.jinja (auto-normalises W vs kW):
    - name: "<X> cost rate"
      unique_id: <x>_cost_rate
      unit_of_measurement: SEK/h
      state_class: measurement
      state: >-
        {% from 'energy_cost.jinja' import cost_rate %}
        {{ cost_rate('sensor.<x>_power') }}
    
  2. Riemann integration (sensors/…) — accumulated SEK:
    - platform: integration
      name: "<X> cost"
      unique_id: <x>_cost
      source: sensor.<x>_cost_rate
      unit_time: h
      method: trapezoidal
      max_sub_interval: { minutes: 5 }
    
  3. Utility meters (helpers/utility_meter.yaml) — <x>_cost_daily / _monthly, source: sensor.<x>_cost.

Then read sensor.<x>_cost_daily / _monthly on the dashboard.

When the plug's power channel lies

Some Zigbee metering plugs emit spurious spikes on their power channel while their energy accumulator stays clean. Billing the power channel turns those phantom watts into real SEK, and the error is invisible because the kWh figures (fed from the energy counter via the <x>_energy_daily utility meters) stay correct while the SEK figures drift upward.

Check for it by comparing the two: mean power × 24 h should match the energy counter's daily change. If it doesn't, insert a derivative sensor and cost that instead of the power channel:

- platform: derivative
  name: "<X> power derived"
  unique_id: <x>_power_derived
  source: sensor.<x>_energy      # the kWh counter, not the W channel
  unit_time: h
  time_window: "00:05:00"
  max_sub_interval: { minutes: 5 }

then point step 1's cost_rate() at sensor.<x>_power_derived. max_sub_interval is required — without it the derivative holds its last non-zero value once the counter stops advancing (i.e. when the appliance is off) and bills forever. The derivative's unit is kWh/h, which the macro already reads as kW. sensor.dehumidifier_cost is wired this way.

Custom Jinja macros live in custom_templates/ — HA does not reload them with reload_all; just reload/deploy calls reload_custom_templates first. Adding/removing platform: sensors (integration, statistics, history_stats) requires a full restart — use just restart (or just redeploy = sync + restart), not just deploy.

Secrets

Encrypted at rest with sops/age, using the same two recipients as the infra repo — one operational key on nikki, one disaster-recovery key kept offline. Deliberately the same scheme: a second key-management story for one file is a second thing to get wrong on the day something has to be rotated in a hurry.

secrets.sops.yaml is committed. YAML mode, so keys stay cleartext and only values are encrypted — the file is its own schema, and a rotation is a legible diff without decrypting anything.

just edit-secrets     # opens decrypted in $EDITOR, re-encrypts on save

Home Assistant cannot read it. HA has no notion of sops; it reads secrets.yaml as plaintext when it loads its config. So just sync decrypts on nikki and streams the result to the box:

nikki                                    homeassistant
─────                                    ─────────────
secrets.sops.yaml ──sops -d──► (pipe) ──ssh──► /homeassistant/secrets.yaml (0600)

The plaintext never touches nikki's disk, and it is on stdin rather than in argv so it reaches neither machine's ps. This is the shape infra already uses, where kate holds no key and only ever receives already-decrypted secrets.

It prints what it did, and it is the first thing sync runs:

secrets: 1 key(s) → root@homeassistant.home.arpa:/homeassistant/secrets.yaml

The decrypt is checked before the remote file is opened, and that is not fussiness. sops -d … | ssh … "cat > secrets.yaml" reads better and silently destroys the file it is meant to write: a pipeline exits with the status of its last command, so a failed decrypt still truncates secrets.yaml to nothing and reports success. Measured, not feared. So the value is decrypted in full and checked for at least one key first, then written to a temp file on the box and mvd into place, so an interrupted transfer cannot leave a half-written secrets file for HA to load.

What it buys is a repo that holds ciphertext. What it does not buy is a box with no plaintext on it — HA needs the cleartext to start, and that is the end of it. secrets.yaml stays gitignored; the copy on the box is the only one.

Decryption needs the age key, so deploys run from nikki. Anywhere else the decrypt fails, the recipe refuses, and the box keeps the secrets.yaml it already had. Editing that file directly on the box is pointless — the next sync overwrites it.