Skip to content

Latest commit

 

History

35 Commits

Folders and files

Repository files navigation

PositionGuard — Hubitat Elevation Integration

Privacy-first family location for Hubitat. PositionGuard exposes your family group's presence as native Hubitat presence devices, with area-level granularity — home, school, grandma's house — instead of exact coordinates.

Built for Hubitat users who want reliable family presence detection without handing their family's location data to third-party services.


What's different about this

  • Area-level, not coordinate-level. PositionGuard reports presence as "in this area" or "not in this area" — never exact GPS coordinates.
  • One install per person. Family members install the PositionGuard app on their phone once. They don't need Hubitat accounts or any Hubitat configuration on their device.
  • Works around iOS Private Wi-Fi Address rotation. Position is reported by the phone, not detected by the router, so iPhones report reliably regardless of MAC randomization.
  • Privacy-first by design. No data sale, no ads, no third-party trackers. Sharing can be paused per-person at any time.

What this integration provides

One parent app (PositionGuard) that holds your API key, polls the PositionGuard API, and manages two kinds of child device:

  • One member device per group member, using the PositionGuard Member driver — presence, current area, and Safety Zone status.
  • One area count device per area in each selected group, using the PositionGuard Area driver — how many members are currently inside that area, and (if you turn it on) a button that moves the area to your phone's location.

Each member device implements Hubitat's PresenceSensor and Refresh capabilities and exposes:

Attribute Values Meaning
presence present / not present Whether the member is in the device's designated presence area (see below)
currentArea area name, away, or unknown The member's area-level state — see the table below
areaSince ISO-8601 UTC timestamp When the hub observed the current area state begin
areaSinceLocal yyyy-MM-dd HH:mm:ss, hub-local The same instant as areaSince, rendered in the hub's local time zone — for dashboards
sharingStatus active / disabled Whether the member has paused location sharing
safetyStatus at_area / in_zone / out_of_zone / unknown The member's position relative to their own usual area — see "Safety Zone status" below
outsideUsualArea true / false true exactly when the member is confirmed outside their usual area — a direct Rule Machine trigger, no comparison logic needed
positionFresh true / false Whether the member's most recent position is recent enough for PositionGuard to trust it. false means the phone has gone quiet — including while safetyStatus holds at_area from a last-known position
positionAgeSeconds integer Seconds since the member's last reported position, as of the last poll

Every state change carries a human-readable descriptionText (e.g. "Sally arrived at School"), so device event logs and notifications read naturally.

currentArea values

currentArea value Meaning
area name Member is inside that area
away Member is sharing, and in no defined area
unknown Member's sharing is paused — no current knowledge

away follows Hubitat convention for dashboards and event logs. (The Home Assistant integration names the same state in HA's own jargon; cross-platform verbatim matching is not a goal.)

The presence area

Hubitat presence is binary, unlike PositionGuard's multi-area model. Each member device therefore has a Presence area preference: the area name that maps to present (matched case-insensitively). Set it per device on the device page — e.g. "Home" for most members, "School" if you want a device that means "Sally is at school".

Default when unset: an area named "Home" is used. If the member is never in an area named "Home", their presence stays not present. For any-area automations, use the currentArea attribute instead of presence.

Safety Zones status

PositionGuard computes each member a usual area — a zone derived from that member's own saved places, weighted by where they actually spend time. Two attributes expose the member's position relative to it:

safetyStatus value Meaning
at_area At one of their saved places
in_zone Not at a saved place, but inside their usual area
out_of_zone A fresh position exists, and it is outside their usual area
unknown Nothing trustworthy to report — see below

outsideUsualArea is deliberately just true / false, so a Rule Machine trigger on it fires exactly when someone is confirmed outside their usual area — and at no other time. unknown maps to false: absence of data is never evidence of being outside, and a dying battery must never set off an outside-zone alert.

safetyStatus reads unknown in four situations, none of which is "safely inside":

  • The phone has gone quiet. No position has been reported recently enough for PositionGuard to trust it. positionFresh reads false and positionAgeSeconds tells you how long it's been. This is the common overnight case — a sleeping phone is not a missing person.
  • The member paused sharing.
  • No selected group is allowed to carry safety data — the member muted the group, or the group is public.
  • Safety Zone status isn't live on their account yet.

A quiet phone last confirmed inside one of the member's saved places is the exception: PositionGuard holds at_area rather than flapping to unknown overnight. positionFresh reads false while it does, and the event log says "Sally was last confirmed at a saved place 10 h ago". Rules that must know someone is there right now should also require positionFresh to be true.

Rules that want to react to a phone going quiet — a low-battery reminder, say — should trigger on positionFresh changing to false, not on safetyStatus. Keep outsideUsualArea for what it's for.

Area member counts

Each area in a selected group gets its own child device (the PositionGuard Area driver) reporting how many members are inside it right now. The device knows how many, never who or where — it's built for occupancy rules, not tracking.

Attribute Values Meaning
memberCount integer Members currently inside this area, including any whose position is stale
staleCount integer How many of those are counted from a position that has gone quiet
freshCount integer memberCount minus staleCount — members counted from a position PositionGuard still trusts
countAvailable true / false Whether the counts are current. false when they couldn't be fetched — the numbers then hold their last value rather than dropping to zero

A member who paused sharing, or who has this area set as a private place, is not counted — the count only reflects what members have chosen to share.

Count devices are named <Group>: <Area> count (e.g. Family: Home count) and are created and removed automatically as areas are added to or removed from a selected group.

When a group is no longer selected. If you deselect a group, or your API key can no longer see it (you left the group, or it was deleted), its area devices are kept, not deleted, so rules that use them don't break. On the next poll their countAvailable changes to false, and their numbers stay at the last value, as they do during an outage. Pushing the button on one of them doesn't move anything: lastMoveResult says to select the group again or remove the device. Select the group again in the PositionGuard app on your hub (Apps → PositionGuard → Groups) and they come back on the next poll. If you don't need them any more, remove them yourself under Devices, after checking that no rule uses them.

Why staleCount exists. A member counted from a stale position is probably still there — that's the usual reason a phone goes quiet at home overnight — but "probably" is worth knowing about. If a rule should act only on confirmed presence, use freshCount (which is exactly memberCount minus staleCount), and gate it on countAvailable being true so an API outage can't be read as an empty house.

Moving an area from a dashboard button

Each area count device also has a button. Pushing it moves that area to where your phone last reported, so a "move Hotel here" tile is one tap. The area keeps its name and radius; only its centre moves.

Two switches, both off until you turn them on:

  1. In the PositionGuard app on your hub (Apps → PositionGuard): Allow this hub to move areas (under Moving areas). While it's off, pushing the button does nothing except set lastMoveResult to say so.
  2. An API key created with Allow this key to move areas you created at dev.positionguardai.com. A key's permissions can't be changed after it's created, so an older key needs replacing: create a new one, then enter it under Re-enter API key. The hub can't check a key's permissions in advance. If the key lacks this one, PositionGuard says so the first time you push, and that message is what lastMoveResult shows.

Position age limit. Also under Moving areas: how old your phone's last reported position may be when you push the button — 2 minutes (the default), 5 minutes or 15 minutes. A phone that isn't moving reports less often, so its last position can be a few minutes old and the button is refused. A longer limit lets it work anyway, but it may place the area where you were rather than where you are, up to that long ago, and that's the place your group sees.

Every area device gets the button, including areas you didn't create. PositionGuard only lets the person who created an area move it. On anyone else's area, pushing the button shows PositionGuard's answer, "Only the person who created this area can move it." If an area is linked to two of your selected groups, it has two devices, and pushing either one moves the same area.

Attribute Values Meaning
lastMoveResult a sentence, or No move yet What happened the last time the button was pushed
lastMoveAt ISO-8601 UTC timestamp, or never When the area last actually moved. Refusals and "already there" leave it alone

Before the first push the two read No move yet and never, so a rule can refer to them from the start. Both attributes come from the PositionGuard Area driver: update it along with the app (HPM does both), or the device won't have them.

A move reads like "Hotel moved 341 km (212 mi) to your phone's location", or "412 m (1353 ft)" under a kilometre. When the area is already within 1 m of your position, it isn't moved and nothing is sent to your group.

When PositionGuard refuses, lastMoveResult shows its message word for word. It refuses when:

  • the area is archived,
  • your location sharing is off,
  • it has no position for you,
  • your last position is older than your position age limit (2 minutes unless you change it),
  • it can't tell how accurate your last position was, or
  • your last position is less precise than the area is wide.

It also refuses when the area isn't yours to move, when the area isn't found, and when the area was moved less than 30 seconds ago. A refused move never affects polling or your presence devices.

Privacy. The hub never sends or receives coordinates: the request has no body, and the answer is a distance and an age. But moving an area to yourself tells the people you share it with where you are. Everyone in the area's groups sees its new centre, exactly as if you'd moved it in the PositionGuard app on your phone. That's why moves are refused while your sharing is off.

Making it a one-tap button in HD+. The device has Hubitat's PushableButton and Momentary capabilities. In HD+, a tap on a button device opens a dialog by default. To make one tap push it, long-press the tile, choose Edit, and set its click action to Toggle (HD+: Buttons, Click Action). HD+ picks a device type from the device's attributes. If it doesn't show this device as a button, change its device type from the same Edit screen (Change Device Type).

Android Auto: HD+ lets you choose which devices appear in the car (HD+: Android Auto). Whether this button works with one tap from Android Auto is unconfirmed until a user reports back. If you try it, please tell us in the community thread.


Compatibility

  • Hubitat Elevation: platform version 2.3.0 or later
  • PositionGuard phone app: latest version on the App Store or Google Play. Android is in Google Play open testing (US, UK and Sweden for now); if you're in another region or the test is full, mention it in the Discussions tab and I'll help you get access. Safety Zone status needs iOS 2.0 / Android 0.9 or later; area member counts are computed server-side and have no app-version requirement.
  • Hubitat Package Manager (HPM): recommended for installation, though manual install is supported

Account and usage limits

The integration requires a PositionGuard account (free) and an API key from dev.positionguardai.com. Free tier covers a typical household:

  • Up to 3 groups
  • Up to 3 areas per group
  • Up to 10 members per group

An optional paid tier on iOS raises these limits (unlimited groups and members, up to 20 areas per group) for larger setups.

The integration polls the PositionGuard API once per minute by default (configurable to 5 or 10 minutes), which is well within the API's rate limits — no tuning needed. If you also run the Home Assistant integration, use a separate API key per integration so keys can be rotated independently.


Installation

You'll go through three places: the PositionGuard app on your phone (to set up your account and family), the developer portal (to mint an API key), and your Hubitat hub (to install this integration).

1. Install the PositionGuard phone app and set up your family

  1. Install PositionGuard from the App Store or Google Play.
  2. Sign in with your phone number (SMS verification).
  3. Create a family group. Default name is "Family"; rename if you like.
  4. Add areas to the group: at minimum a "Home" area centered on your house. Add others as needed (work, school, grandma's house, etc.).
  5. Invite family members to the group. They install the PositionGuard app on their phones and accept the invite. Sharing can be paused per-person at any time.
  6. Confirm positions update on the phone app's map before checking Hubitat. Location sharing is off by default for every member (including you) — each person turns it on in the PositionGuard app on their phone when they're ready. A member who hasn't enabled sharing yet appears in Hubitat as currentArea: unknown / sharingStatus: disabled, which is the integration working, not a bug.

Setting up several family phones at once? Account signup verifies each phone number by SMS, and the verification provider applies anti-abuse limits that can flag many signups done back-to-back. Onboarding a whole household? Stagger the signups — a couple of phones, then a break. If a number does get temporarily blocked, don't retry repeatedly (retries can extend the cooldown); wait a few hours and it clears on its own.

2. Get your API key from the developer portal

  1. Visit dev.positionguardai.com.
  2. Sign in with the same phone number you used for the app.
  3. Click Create API key, give it a descriptive name (e.g., "Hubitat").
  4. Copy the key — it's shown once only. Store it somewhere safe (a password manager works well).

3a. Install via Hubitat Package Manager (recommended)

  1. Install Hubitat Package Manager if you don't already have it.
  2. Open HPM → Install → From a URL.
  3. Paste the manifest URL: https://raw.githubusercontent.com/positionguard/positionguard-hubitat/main/packageManifest.json
  4. Follow the prompts. HPM installs the PositionGuard hub app and both drivers, and will offer future updates.

3b. Manual installation (without HPM)

  1. In Hubitat, open Drivers Code → New Driver → Import, and paste: https://raw.githubusercontent.com/positionguard/positionguard-hubitat/main/drivers/positionguard-member.groovy Save.
  2. Repeat for the Area driver: Drivers Code → New Driver → Import, and paste: https://raw.githubusercontent.com/positionguard/positionguard-hubitat/main/drivers/positionguard-area.groovy Save. (Install both drivers first — the app creates child devices with them.)
  3. Open Apps Code → New App → Import, and paste: https://raw.githubusercontent.com/positionguard/positionguard-hubitat/main/apps/positionguard-app.groovy Save.

Updating manually: an update must cover all three files — updating only some leaves the app and drivers on mismatched versions, which shows up as a red MissingMethodException in the logs (on member updates for the Member driver, on area-count device creation for the Area driver) until they match again. Drivers first here too: open Drivers Code → PositionGuard Member → Import → Save, then PositionGuard Area → Import → Save, then Apps Code → PositionGuard → Import → Save. The import URL is remembered from installation, so each is a one-click re-fetch — no pasting needed.

4. Configure the integration

  1. Go to Apps → Add User App → PositionGuard.
  2. Enter your API key. It is validated against PositionGuard immediately — you can't proceed with an invalid key.
  3. Select which group(s) to integrate. You can select multiple groups; a member in several selected groups still gets a single device.
  4. Press Done. Within one poll interval (about a minute), one presence device per member appears, named after the member, plus one count device per area in each selected group.
  5. On each member's device page, set the Presence area preference if something other than "Home" should count as present.

Renaming a member in PositionGuard updates the Hubitat device's name automatically; the device itself (and your rules) are preserved. The device label is yours: set one on the device page and it sticks — the integration never overwrites it. Clear the label to display the PositionGuard nickname again. Members removed from all selected groups have their device deleted.


Rule Machine examples

Welcome someone home — trigger on the presence capability: Trigger: Sally presence · arrives → Action: turn on the hallway lights. (With Sally's Presence area left at the default "Home".)

Notify when a child reaches school — trigger on the custom attribute: Trigger: Custom Attribute → device Sally → attribute currentArea → value School → Action: send "Sally arrived at school". Because currentArea carries every area transition, one rule per area is all you need — no extra devices.

Announce any transition — trigger on currentArea changed and use %value% (the new area, away, or unknown) in the notification text.

Alert when someone is outside their usual area — trigger on the Safety Zone boolean directly: Trigger: Custom Attribute → device Sally → attribute outsideUsualArea → value true → Action: send "Sally is outside her usual area". No comparison logic needed — the attribute is true only on a confirmed out_of_zone, so this rule never fires on a quiet phone or missing data (both read as unknown).

Away mode when the house is empty — trigger on the Home area's count device: Trigger: Custom Attribute → device Family: Home count → attribute memberCount → value 0 → Action: set mode to Away. Add a condition that countAvailable is true, so an API outage — where the count holds its last value rather than dropping to zero — is never read as an empty house. One device, one attribute, no per-member logic. To act only on confirmed presence, trigger on freshCount → 0 instead (or add a condition that staleCount is 0).


Polling and reliability

  • Polls every minute by default; 5- and 10-minute intervals are available in the PositionGuard app on your hub. Changes typically reflect within one poll interval.
  • Transient failures (network down, timeouts, server errors, rate limiting): child devices hold their last-known state, a warning is logged, and presence never flaps. Normal updates resume on the next successful poll.
  • Invalid or revoked API key: polling stops, the error is shown at the top of the PositionGuard app on your hub, and a single error is logged (no once-a-minute spam). Re-enter and validate the key there to resume.

What happens when someone pauses sharing

A pause is a meaningful unknown, not an error state — the same semantic direction as the Home Assistant integration. When a member pauses sharing in the PositionGuard app on their phone, on the next poll:

  • presence holds its last value. A pause must never fire a departure (or arrival) automation, so presence-based rules stay quiet for the whole pause.
  • currentArea becomes unknown. Area knowledge genuinely lapsed, and that staleness is visible on dashboards rather than a stale area name. Rules triggered on currentArea may fire at this point — area knowledge really did change.
  • sharingStatus becomes disabled, and one event narrates the transition ("Sally's location sharing is paused"). To react to pauses explicitly, trigger on this attribute.

A member first seen while already paused (e.g. added to a group during their pause) starts as unknown / disabled / not present: there is no prior presence to hold, and not-present is the safe default because a false arrival is worse than a false absence.

When sharing resumes, real values return on the next poll. If the member's area or presence changed while paused, those events fire once at that point — the change is real; it just became knowable.


Privacy

PositionGuard is built privacy-first. The integration shows presence at area level only, never exact coordinates.

What's shared with your hub:

  • Whether each member is in any area of a selected group
  • Which specific area, if any, they're in (currentArea)
  • Whether sharing is active or paused
  • The member's Safety Zone status and position freshness — status words, booleans, and a duration; never a location
  • How many members are in each area of a selected group (a count only)

What's never shared:

  • Exact GPS coordinates
  • Movement history outside of area transitions
  • Any data about non-family-members nearby

Concretely, for this Hubitat integration: no coordinate data ever appears in any attribute, event, log line, or state variable — at any log level, including debug. The integration calls three endpoints: the group list, the group members, and the per-area member counts. Their responses contain area names and counts only; the integration never requests the endpoint that describes area geometry. If you turn on area moves, pushing an area device's button calls a fourth, the area move. It sends no body and gets back a distance and an age, never a place (see "Moving an area from a dashboard button"). You can verify this in the source — it's a grep away. The Safety Zone attributes keep the same contract: safetyStatus, outsideUsualArea, and positionFresh are a status word and two booleans, and positionAgeSeconds is a duration — the usual area's location, size, and shape are computed and kept by PositionGuard and never reach the hub in any form. Area counts are likewise a number: which members make up that number is not part of the response.

When a family member pauses sharing in the PositionGuard app on their phone, the integration respects this immediately (see "What happens when someone pauses sharing" above).


Limitations

This integration is read-only, with one opt-in exception: moving an area you created to your phone's location (see "Moving an area from a dashboard button"). From Hubitat, you cannot create, rename, resize, or delete groups, areas, or members; change sharing permissions; or send messages or invitations. These actions remain in the PositionGuard phone app, where group members manage their own privacy directly.

The integration is polling-based (the PositionGuard backend has no webhook support), so state changes appear within one poll interval — about a minute at the default setting.


Questions, feedback, bug reports

Use the Discussions tab for questions or feedback, and Issues for bug reports.

For information about PositionGuard the app or the developer portal, visit positionguardai.com.


License

MIT — see LICENSE.