Mast and Home Assistant
Home Assistant knows when there is water on your basement floor. Telling you about it at four in the morning, from a house with no open ports and no cloud subscription, is a different problem.
Mast is a pager for your iPhone. An automation sends a page, the phone rings until somebody acknowledges it, and Home Assistant can read that acknowledgement back. Nothing has to reach your house from the internet: every request goes outwards, from your Home Assistant to mast, and the phone talks to mast rather than to your house.
You do not need a custom integration for any of this. Everything on this page uses rest_command, which has been part of Home Assistant core for years.
There is one if you want it. Mast for Home Assistant installs through HACS and makes the same calls, and it adds the part that is awkward in YAML: mast.page waits for the acknowledgement and returns it to the automation, and each channel gets entities for who answered and whether a page is still open. Everything below works without it.
Who this is for
You run Home Assistant yourself — on a Pi, a mini PC, an old laptop in a cupboard. You have no Home Assistant Cloud subscription and no ports forwarded, and you would like to keep it that way. Maybe you reach it over Tailscale or WireGuard when you are out, maybe you do not reach it at all.
That setup is the one mast fits best, because the only thing it asks of your network is the ability to make an ordinary outgoing HTTPS request.
There is a short section at the end for people who do have Home Assistant Cloud. It is still worth reading the rest first: the cloud changes less about this than you would think.
What the Home Assistant app already does, and where it stops
Keep the Home Assistant companion app. It is free, it does rich notifications with images and buttons, and it is the right place for almost everything your house says. This page is not about replacing it.
It stops in three places, and none of them are bugs — they are what a notification is.
A notification is one shot. It is sent, and that is the end of it. Nothing repeats, nothing escalates, and nothing anywhere records whether you saw it. If you slept through it, your house does not know.
Tapping a button needs a path back to your house. The companion app's actionable notifications work by firing a mobile_app_notification_action event back at your instance. On your own wifi that is a local connection. Away from home, without remote access, the tap has nowhere to go.
It cannot tell you about itself. If Home Assistant has crashed, the SD card has died, or the power is out, the notification you most wanted is the one nothing is left to send.
There is also a ceiling worth knowing about: the companion app allows 500 push notifications per device per day, reset at midnight UTC. That is plenty for normal use, and it is exactly the sort of thing a stuck sensor eats in an afternoon.
Mast is a second, deliberately separate path. Different app, different servers, and a page that repeats until a person answers it. Because the phone talks to mast instead of to your house, acknowledging a page works from hotel wifi, from a car, from a phone with no VPN and no idea what your internal IP range is.
A rule of thumb for deciding which app something belongs in:
| What happened | Where it should go |
|---|---|
| The washing machine finished | Home Assistant app |
| Somebody rang the doorbell | Home Assistant app |
| The 3D print is done | Home Assistant app |
| The front door unlocked at 02:00 | Mast, at loud |
| There is water on the basement floor | Mast, at page |
| The freezer has been above -12 C for ten minutes | Mast, at loud |
| Home Assistant itself has stopped | Mast, as a vital — nothing else can |
The ten-minute setup
1. Make a channel
Install Mast, tap New channel, and give it a name like house. The app hands you a send address that looks like this:
https://mast.tissue.dev/mk_8e2ab3c4d5e6f708192a3b4c5d6e7f8091a2b3c4That URL is the whole credential. Anyone who has it can ring your phone, so treat it like a password: it goes in secrets.yaml, not in a forum post and not in a screenshot.
There is no Tissue account to create for this and no API token to manage. A channel and a URL is all of it.
2. Put it in secrets.yaml
# secrets.yaml
mast_url: "https://mast.tissue.dev/mk_8e2ab3c4d5e6f708192a3b4c5d6e7f8091a2b3c4"3. Add three commands to configuration.yaml
# configuration.yaml
rest_command:
# An ordinary notification. Nothing repeats.
mast_notify:
url: !secret mast_url
method: post
content_type: "application/json"
payload: >-
{{ {"title": title,
"body": body | default(""),
"priority": priority | default("normal"),
"key": key | default("")} | to_json }}
# A page. Rings until somebody acknowledges it on the phone.
mast_page:
url: !secret mast_url
method: post
content_type: "application/json"
payload: >-
{{ {"title": title,
"body": body | default(""),
"priority": "page",
"key": key,
"expire": expire | default(1800)} | to_json }}
# "It is fixed" — closes the page with this key and turns the card green.
mast_resolve:
url: !secret mast_url
method: post
content_type: "application/json"
payload: >-
{{ {"title": title | default("Resolved"),
"body": body | default(""),
"resolve": "1",
"key": key} | to_json }}to_json does the quoting for you, so a body containing an apostrophe, a newline or a temperature reading will not break the request.
4. Reload, and try it
Restart Home Assistant, or go to Developer tools → YAML and reload REST commands. Then open Developer tools → Actions (called Services on older versions), pick rest_command.mast_notify, and run it with:
title: Hello from Home Assistant
body: If you are reading this on your phone, it works.
priority: normal
key: setup-testYour phone should buzz within a second or two. If it does not, skip to when it does not work.
Now try rest_command.mast_page with title: Test page and key: setup-test-page. That one keeps ringing until you swipe Acknowledge, which is the whole point of the thing.
The automations below use the newer
triggers:/actions:spelling. If your Home Assistant predates 2024.10, writetrigger:/action:and useservice:where these sayaction:. The contents are identical.
Your first real automation
automation:
- alias: Water in the basement
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.basement_leak
to: "on"
for: "00:00:30"
actions:
- action: rest_command.mast_page
data:
title: Water in the basement
body: "Leak sensor by the water heater. Main valve closing in 60 seconds."
key: leak-basement
- delay: "00:01:00"
- action: switch.turn_off
target:
entity_id: switch.main_water_valveThree things in there are doing real work.
for: "00:00:30" means the sensor has to stay wet for thirty seconds. A sensor with a loose lead that flickers on and off does not fire this automation at all.
key: leak-basement is the dedupe key. It names the situation, not the message. If the automation fires again while the first page is still on the phone, mast folds the second one onto the first instead of ringing again. This field matters more than anything else on this page, and the next section is entirely about it.
mode: single stops Home Assistant from running two copies of this automation at once.
And when the leak stops:
automation:
- alias: Basement dry again
triggers:
- trigger: state
entity_id: binary_sensor.basement_leak
to: "off"
for: "00:05:00"
actions:
- action: rest_command.mast_resolve
data:
key: leak-basement
title: Basement dry
body: Sensor has been clear for five minutes.The card on your phone goes green, stops ringing and stops escalating, and the notification says how long it was open. If nothing was open, nothing bad happens: the resolve is filed as a silent note in the feed and wakes nobody.
Making it actually wake you
This is the part people skip and then discover at 4 a.m.
A mast page is delivered as a time-sensitive notification, which breaks through a Focus — Sleep included. To also ring through the physical silent switch on the side of the phone, the page has to be delivered as a critical alert, and that happens only on a phone where you have turned critical alerts on for Mast.
In the app, tap the gear on the feed to reach Settings. There is a row for critical alerts with a Turn on button; iOS asks its own permission question, once. The app tells you where it stands rather than assuming — the line under the page tier reads time sensitive · breaks through Focus and turns amber when a page could go unheard, and its button opens a checklist of the five settings that decide the outcome.
Two more things belong to the phone, not to your YAML:
The channel's ceiling. Every channel has a maximum priority, set in the app on the phone that owns it. A send asking for page on a channel whose ceiling is loud is stored and delivered as loud — no error, because the point of the ceiling is that whoever holds the sending key cannot raise it. If a page arrives quietly, this is the first thing to check.
A test that tests the real thing. The app can send itself a test at a chosen tier, up to the channel's ceiling. Do that once, with the phone face-down on a nightstand and the ringer switch off, before you trust it with the sump pump.
The full list of what makes a page audible is in making a page wake you.
Choosing a priority
| Priority | What the phone does | Use it for |
|---|---|---|
quiet |
Appears in the list, no sound, no lit screen | A log line you want to be able to look up later |
normal |
An ordinary notification | The sort of thing the Home Assistant app already handles |
loud |
Time-sensitive: breaks through Focus and Sleep | Wake somebody up, once |
page |
Repeats until acknowledged, ignores mute and quiet hours | Something that will get worse until a person does something |
A page is the only tier that asks for an answer, and the only one that keeps asking. Use it sparingly — three pages a month that all mattered is a pager, and thirty pages a month is an app your household turns off.
Flapping sensors: the one thing to get right
A reed switch with a tired magnet, a leak probe with a loose lead, a Z-Wave sensor at the far end of a weak mesh: any of these can change state once a second for hours. An automation with no for: and no key: turns that into one send per flip.
One send a second is sixty a minute, and sixty a minute is exactly the ceiling: a channel key is allowed 60 requests per minute, shared between sending and reading answers back. Past that, mast answers 429 and stores nothing. By then your phone has buzzed sixty times, your feed has sixty rows, and the actual problem — a loose lead — is buried under them.
What a dedupe key does
Every send carrying the same key inside the channel's dedupe window is folded onto the first one. Folded means no second notification, no second row, no second buzz: the card already on your phone updates in place with a count, so it reads ×47 instead of arriving forty-seven times.
The window is five minutes by default, per channel, and you can set it up to a day in the app.
A good key names the situation and stays the same across firings:
key: leak-basement
key: freezer-warm
key: door-garage-open
key: ups-on-batteryA bad key is one that changes every time, because that is the same as having no key at all:
key: "leak-{{ now().timestamp() }}" # a brand new key every secondFive guards, strongest first
1. Do not let the trigger fire on noise. for: costs nothing and fixes most of it.
triggers:
- trigger: state
entity_id: binary_sensor.basement_leak
to: "on"
for: "00:00:30"2. Put a key on every send. All the examples on this page have one.
3. Do not send again while the last one is unanswered. Keep a helper that the automation sets when it pages and clears when the thing recovers, and refuse to send while it is on:
# configuration.yaml
input_boolean:
leak_page_open:automation:
- alias: Water in the basement
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.basement_leak
to: "on"
for: "00:00:30"
conditions:
- condition: state
entity_id: input_boolean.leak_page_open
state: "off"
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.leak_page_open
- action: rest_command.mast_page
data:
title: Water in the basement
body: Leak sensor by the water heater.
key: leak-basementClear it in the recovery automation, next to the mast_resolve call.
4. Throttle with a timer for anything chatty. A timer helper is the simplest rate limit Home Assistant has:
# configuration.yaml
timer:
mast_cooldown:
duration: "00:15:00" conditions:
- condition: state
entity_id: timer.mast_cooldown
state: idle
actions:
- action: timer.start
target:
entity_id: timer.mast_cooldown
- action: rest_command.mast_notify
data:
title: Basement humidity high
body: "{{ states('sensor.basement_humidity') }} percent"
priority: normal
key: basement-humid5. Notice when it happens. Every send answers with "duplicate": true when it was folded. Log that rather than paging about it — the answer to too many alerts is never one more alert:
- action: rest_command.mast_notify
data:
title: Freezer warming
body: "{{ states('sensor.garage_freezer_temp') }} C"
key: freezer-warm
response_variable: sent
- condition: template
value_template: "{{ sent.content.duplicate | default(false) }}"
- action: logbook.log
data:
name: Mast
message: "freezer-warm folded — the sensor is chattering"What mast does if you get it wrong anyway
There is a backstop. Past roughly 20 messages in five minutes on one channel, mast stops pushing each one and folds them into a single summary card that later messages refresh in place. Nothing is thrown away: the feed still keeps every message, and both numbers are adjustable per channel.
One exemption matters enough to say plainly: a page is never folded this way. A page that gets suppressed is a page that does not exist, and a pager may not do that. So the backstop saves you from a flapping loud sensor and does nothing at all for a flapping page. On a page, your dedupe key is the only thing between you and a very bad night.
The budget, in one table
| Requests per channel key | 60 a minute, sends and reads together |
| Folded duplicates | Cost a request; no notification, no feed row |
| Message body | 16 KiB |
| History kept | 30 days |
A sensible automation sends single-digit messages a day and never sees any of these.
Mast watching Home Assistant
This is the thing the companion app cannot do for you, and for a self-hosted setup it may be the best reason on this page.
A vital is a channel that expects to hear from you on a schedule. Home Assistant beats it every fifteen minutes. If the beat stops — the Pi died, the power went out, the internet is down, Home Assistant is stuck in a boot loop — mast notices the silence and pages you.
Create a vital channel in the app: New channel, and pick Vital instead of the ordinary kind. A new vital expects a heartbeat every hour and allows ten minutes of grace, which is a good fit for this without changing anything — beat it every fifteen minutes and a missed beat has to be missed four times over before mast says anything. The app hands you a second URL.
# secrets.yaml
mast_heartbeat_url: "https://mast.tissue.dev/mk_1c9f…"# configuration.yaml
rest_command:
mast_heartbeat:
url: !secret mast_heartbeat_url
method: postautomation:
- alias: Tell mast Home Assistant is alive
triggers:
- trigger: time_pattern
minutes: "/15"
actions:
- action: rest_command.mast_heartbeatThat is the whole thing. A beat is a send with no text, so there is nothing to configure and nothing to read.
You will hear about a real outage within about seventy minutes, and you will hear nothing at all about a routine restart. To hear sooner, open the channel in the app and shorten Heartbeat period — but keep the grace at three or four times the beat, because Home Assistant updates itself, reboots, and occasionally takes a few minutes to come back, and a grace window shorter than that pages you every upgrade night.
When it comes back, the next heartbeat closes the page on its own: the red card turns green and tells you how long the silence lasted. Nothing else to write.
You can also make a shutdown say so, which turns "my house is offline" into "I turned my house off":
automation:
- alias: Home Assistant is stopping
triggers:
- trigger: homeassistant
event: shutdown
actions:
- action: rest_command.mast_notify
data:
title: Home Assistant shutting down
body: "Restart or power off at {{ now().strftime('%H:%M') }}."
priority: quiet
key: ha-shutdownWaiting for an answer
Everything above is one-way: send, and carry on. Mast can also tell you what the person did, which lets an automation wait for a human before it acts.
Add one more command. It needs the message URL, so secrets.yaml gets a second line with the same key in it — the {{ id }} is filled in when the command runs:
# secrets.yaml
mast_status_url: "https://mast.tissue.dev/mk_8e2ab3c4d5e6f708192a3b4c5d6e7f8091a2b3c4/messages/{{ id }}"# configuration.yaml
rest_command:
mast_status:
url: !secret mast_status_url
method: getNow an automation can send a page, wait, and branch on the answer:
automation:
- alias: Ask before closing the valve
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.basement_leak
to: "on"
for: "00:00:30"
actions:
- action: rest_command.mast_page
data:
title: Water in the basement
body: Acknowledge within ten minutes or the main valve closes.
key: leak-basement
expire: 600
response_variable: sent
- repeat:
sequence:
- delay: "00:00:10"
- action: rest_command.mast_status
data:
id: "{{ sent.content.id }}"
response_variable: page
until:
- condition: template
value_template: >-
{{ page.content.state in ['acked', 'resolved', 'expired']
or repeat.index >= 60 }}
- choose:
- conditions:
- condition: template
value_template: "{{ page.content.state == 'acked' }}"
sequence:
- action: logbook.log
data:
name: Mast
message: >-
Acknowledged by {{ page.content.acked_by }} after
{{ (page.content.acked_at | as_timestamp
- page.content.received_at | as_timestamp) | round }}
seconds. Valve left open.
default:
- action: switch.turn_off
target:
entity_id: switch.main_water_valve
- action: rest_command.mast_notify
data:
title: Main valve closed
body: Nobody answered, so the valve was closed automatically.
priority: loud
key: leak-basement-closedWhat that does: page the phone, poll every ten seconds for up to ten minutes, and if nobody swipes Acknowledge in that time, close the valve and say so.
Keep the poll gentle. Every poll spends one of the sixty requests a minute that the channel gets, so ten seconds is comfortable and one second is not. If you want to wait for hours, poll every ten seconds for the first few minutes and then slow down.
If you would rather not own that loop, the HACS integration has one built in: mast.page returns acknowledged, acked_by and open_for into a response_variable, and its polling backs off as a page ages. The requests come out of the same sixty a minute.
The status response is small and readable:
{
"id": "mm_7a3e5b91c04d2f68a1b3c5d7e9f02468",
"state": "acked",
"received_at": "2026-09-17T03:48:12.417Z",
"dedupe_count": 0,
"acked_at": "2026-09-17T03:48:49.102Z",
"acked_by": "Denis's iPhone",
"resolved_at": null,
"expires_at": null
}state is fired while it is still ringing, and acked, resolved or expired once it is over.
Sounds, pauses and quiet hours
These all live on the phone, which is deliberate: the person being woken decides how, and no automation and no key can overrule them.
Five sounds. Mast ships pager, klaxon, rising, chirp and bleep, and the app plays one when you tap it. Set one per channel, or name one on a send with sound: klaxon. Different sounds for different channels is the cheapest way to know what happened before you have looked at anything — a klaxon for water, a chirp for the freezer.
The pause (Mast 1.2). Sometimes the house has to stop ringing for an hour — a meeting, a film, a dentist. A page ignores mute and quiet hours by design, so the pause is the control that answers this honestly: you name a length, pages arriving inside the window are delivered silently, and everything nobody acknowledged goes back out at full volume the moment the window closes. Nothing is dropped and nothing is downgraded. It is on the phone and only on the phone: no send field and no token can open or close one.
Quiet hours soften a channel overnight, so the bin-day reminder does not wake anybody. They are per channel, they are unset until you set them, and a page ignores them on purpose. They can also be set from a channel catalogue if you would rather keep them in a file than in an app.
More examples
Freezer warming up. A ten-minute for: keeps a defrost cycle from paging you.
automation:
- alias: Freezer warming
triggers:
- trigger: numeric_state
entity_id: sensor.garage_freezer_temp
above: -12
for: "00:10:00"
actions:
- action: rest_command.mast_notify
data:
title: Freezer warming
body: "Garage freezer is at {{ states('sensor.garage_freezer_temp') }} C."
priority: loud
key: freezer-warmThe power went out and the UPS is on battery. The house is running on borrowed time, so this one pages.
automation:
- alias: UPS on battery
triggers:
- trigger: state
entity_id: sensor.ups_status
to: "On Battery"
for: "00:01:00"
actions:
- action: rest_command.mast_page
data:
title: Power is out
body: "UPS at {{ states('sensor.ups_battery_charge') }} percent, about {{ states('sensor.ups_battery_runtime') }} minutes left."
key: power-outA door left open. Loud, once, with a key so a wobbly contact cannot repeat it.
automation:
- alias: Garage door left open
triggers:
- trigger: state
entity_id: cover.garage_door
to: "open"
for: "00:20:00"
actions:
- action: rest_command.mast_notify
data:
title: Garage door still open
body: "Open for twenty minutes."
priority: loud
key: door-garage-openNobody is home and a motion sensor went off. A notification carries text, so send the camera as a link and let the tap open it — see the cloud section for the url field this uses.
- action: rest_command.mast_notify_linked
data:
title: Motion in the hallway
body: "Nobody is home, and the hallway sensor fired."
priority: loud
key: motion-hallSend to both apps at once. One script, two notifications: rich and quiet on the companion app, loud and repeating on mast.
script:
tell_me:
sequence:
- parallel:
- action: notify.mobile_app_iphone
data:
title: "{{ title }}"
message: "{{ body }}"
- action: rest_command.mast_notify
data:
title: "{{ title }}"
body: "{{ body }}"
priority: loud
key: "{{ key }}"If you do have Home Assistant Cloud
Home Assistant Cloud is a good product and it solves a different problem. It gives you a working address for your house from anywhere, which makes the companion app's buttons work when you are out and saves you from running a tunnel.
What it does not change:
- A companion notification is still one shot. It does not repeat, does not escalate, and does not record that you saw it.
- The 500-a-day push ceiling is a property of the companion app, not of your remote access.
- If Home Assistant is down, the cloud has nothing to relay. A vital watching your instance is still the only thing that pages you about your instance.
What it adds to this page is worth having: with a public address, you can put a link on the notification so that tapping a page lands you on the right dashboard from anywhere.
rest_command:
mast_notify_linked:
url: !secret mast_url
method: post
content_type: "application/json"
payload: >-
{{ {"title": title,
"body": body,
"priority": priority | default("loud"),
"key": key,
"url": "https://YOUR-ID.ui.nabu.casa/lovelace/water",
"url_title": "Open the water dashboard"} | to_json }}Without the cloud, the same field still earns its place — point it at http://homeassistant.local:8123/lovelace/water and it works on your own wifi, or at your tailnet address and it works wherever your VPN does.
When it does not work
Nothing arrives at all. Run the command by hand from Developer tools → Actions and read the response. A 404 means the key in secrets.yaml is wrong, or the channel was deleted, or the key was rotated more than seven days ago. A 429 means more than sixty requests in the last minute — something is firing in a loop.
The message is in the app but the phone stayed quiet. Check the channel's ceiling in the app: a page sent to a channel capped at loud is delivered as loud. The message in the feed shows what it was actually delivered as. After that, work through the five wake settings — the one that catches most people is the ringer switch, which only a critical alert beats.
A flapping sensor woke the house. Add for: to the trigger, add a key: to the send, and drop the priority from page to loud. Storm control will fold a flapping loud; it will never fold a page.
Mast paged me because Home Assistant restarted. The vital's grace window is too short. Three or four times the beat period is about right.
A template error on send. Build the payload with to_json, as above, rather than by pasting values into a string. A body with a quote, a newline or an ampersand in it is fine that way and breaks the other way.
It worked and then stopped after a key rotation. A rotated key keeps working for seven days, which is your window to update secrets.yaml. After that it is a 404.
Every message mast accepted is in the app's feed for thirty days, including the ones that arrived silently and the ones that were folded, so the feed is usually the fastest place to find out what your automation actually sent.
Where to go next
- Mast and your Tissue account — every send field, the priority tiers, sounds, vitals, incident lifecycle and limits.
- Channels as code — keeping channels, quiet hours and vitals in a file instead of in an app.
- Mast for Home Assistant — the HACS integration: five actions, a notify entity, entities for who answered and whether a page is open, and four blueprints.