Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ on:
- cron: "0 0 * * *"
push:
branches:
- Main
- main
pull_request:
branches:
- Main
- main
Comment thread
TrevorSchirmer marked this conversation as resolved.

permissions: {}
Expand Down
33 changes: 13 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

Backend for the Zone Mapper Lovelace card. Persists zone definitions and exposes per‑zone occupancy sensors based on tracked X/Y entities.

> [!WARNING]
> This integration requires the Zone Mapper lovelace card for functionality. Install it from HACS or from [the repository.](https://github.com/ApolloAutomation/zone-mapper-card)
Starting with 1.1.0, this integration seeds a "Zone Mapper" view with the card on your default dashboard the first time it's set up, so you don't have to drop the card in by hand. The companion [Zone Mapper card](https://github.com/ApolloAutomation/zone-mapper-card) still needs to be installed separately via HACS.

## Features

Expand All @@ -14,11 +13,10 @@ Backend for the Zone Mapper Lovelace card. Persists zone definitions and exposes
- Restores zones, tracked entities, and rotation after Home Assistant restarts
- Listens for and processes updates from the card via a single service
- Auto‑discovers and (re)loads platforms at startup based on existing entities
- Seeds a "Zone Mapper" view with the card on the default dashboard the first time the integration is set up (storage‑mode dashboards only, opt‑out available in integration options)

## Installation

There are two ways to install this integration. Both this integration and [the lovelace card](https://github.com/ApolloAutomation/zone-mapper-card) **must** be installed

### With HACS (Recommended)

HACS is like an app store for Home Assistant. It makes installing and updating custom integrations much easier. Here's how to install using HACS:
Expand All @@ -28,27 +26,22 @@ HACS is like an app store for Home Assistant. It makes installing and updating c
- If HACS is not installed yet, download it following the instructions on [https://hacs.xyz/docs/use/download/download/](https://hacs.xyz/docs/use/download/download/)
- Follow the HACS initial configuration guide at [https://hacs.xyz/docs/configuration/basic](https://hacs.xyz/docs/configuration/basic)

- **Add this custom repository to HACS:**
- **Add both custom repositories to HACS:**

- Go to `HACS` in your Home Assistant sidebar
- CLick on the 3 dots in the upper right corner
- Click on the 3 dots in the upper right corner
- Click "Custom repositories"
- Add this URL to the repository: [https://github.com/ApolloAutomation/zone-mapper](https://github.com/ApolloAutomation/zone-mapper)
- Select `Integration` for the type
- Click the `ADD` button
- Add this URL as type `Integration`: [https://github.com/ApolloAutomation/zone-mapper](https://github.com/ApolloAutomation/zone-mapper)
- Add this URL as type `Dashboard`: [https://github.com/ApolloAutomation/zone-mapper-card](https://github.com/ApolloAutomation/zone-mapper-card)

- **Install Zone Mapper:**

- Go to `HACS` in your Home Assistant sidebar
- Search for `Zone Mapper` in HACS
- Click on the card when you find it
- Click the `Download` button at the bottom right
- Repeat for lovelace card
- Restart Home Assistant
- Go to `Devices and Services`
- In HACS, search for `Zone Mapper` and download both the integration and the card
- Restart Home Assistant when prompted
- Go to `Settings` → `Devices and Services`
- Click `Add Integration`
- Search `Zone Mapper`
- Add `Zone Mapper`
- Search `Zone Mapper` and add it
- Open the new "Zone Mapper" view on your default dashboard, pick the device and target entities on the card, and start drawing

### Manual Installation

Expand All @@ -58,15 +51,15 @@ HACS is like an app store for Home Assistant. It makes installing and updating c
/config/custom_components/zone_mapper
```

2. Add entry to your `configuration.yaml`:
2. (Optional) add entry to your `configuration.yaml` if you want YAML-mode setup:

```yaml
zone_mapper:
```

3. Restart Home Assistant.

4. Companion card: download `zone-mapper-card.js` from [the card repo](https://github.com/ApolloAutomation/zone-mapper-card) under `/config/www` and add it as a Dashboard Resource.
4. Install the companion [Zone Mapper card](https://github.com/ApolloAutomation/zone-mapper-card) (download `zone-mapper-card.js` to `/config/www` and add it as a Dashboard Resource).

## Troubleshooting

Expand Down
42 changes: 39 additions & 3 deletions custom_components/zone_mapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,17 @@
ATTR_X_MIN,
ATTR_Y_MAX,
ATTR_Y_MIN,
CONF_AUTO_CREATE_VIEW,
COORD_SENSOR_UNIQUE_ID_FMT,
DATA_LOCATIONS,
DATA_PLATFORMS_LOADED,
DEFAULT_AUTO_CREATE_VIEW,
DOMAIN,
EVENT_ZONE_UPDATED,
POLYGON_MAX_POINTS,
POLYGON_MIN_POINTS,
PRESENCE_SENSOR_UNIQUE_ID_FMT,
SEEDED_DEFAULT_VIEW_FLAG,
SERVICE_UPDATE_ZONE,
SHAPE_ELLIPSE,
SHAPE_NONE,
Expand All @@ -57,6 +60,7 @@
WARN_RECT_INVALID,
WARN_RECT_NON_NUM,
)
from .frontend import async_seed_default_view

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -488,15 +492,45 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True


def _auto_create_view_enabled(entry: ConfigEntry) -> bool:
value = entry.options.get(CONF_AUTO_CREATE_VIEW, DEFAULT_AUTO_CREATE_VIEW)
return bool(value)


async def _schedule_view_seeding(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Seed the default Lovelace view once HA is fully started."""
if entry.data.get(SEEDED_DEFAULT_VIEW_FLAG):
return
if not _auto_create_view_enabled(entry):
return

async def _run(_event: Event | None = None) -> None:
if entry.data.get(SEEDED_DEFAULT_VIEW_FLAG):
return
if not _auto_create_view_enabled(entry):
return
seeded = await async_seed_default_view(hass)
if seeded:
hass.config_entries.async_update_entry(
entry,
data={**entry.data, SEEDED_DEFAULT_VIEW_FLAG: True},
)

if getattr(hass, "is_running", False):
hass.async_create_task(_run(None))
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _run)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""
Set up Zone Mapper from a config entry.

This registers the service (if not already present) and bootstraps any
restored entities from the registry so platforms load without requiring YAML.
Registers the update service, bootstraps any restored entities, and (once)
seeds a default Lovelace view so new users see the card immediately
(assuming the zone-mapper-card frontend is installed via HACS).
"""
_get_integration_data(hass)
_ = entry

# Ensure service is registered only once across YAML and UI setups.
if not hass.services.has_service(DOMAIN, SERVICE_UPDATE_ZONE):
Expand All @@ -514,6 +548,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, bootstrap_cb)

await _schedule_view_seeding(hass, entry)

return True


Expand Down
54 changes: 48 additions & 6 deletions custom_components/zone_mapper/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
"""
Config flow for Zone Mapper.
Config and options flow for Zone Mapper.

This integration is UI-configurable and requires no options. The flow simply
creates a single entry so users don't need to add `zone_mapper:` to YAML.
The integration is UI-configurable and has a single setup step that just
creates the singleton entry. The options flow exposes a single toggle for the
first-run auto-view seeding.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback

from .const import (
CONF_AUTO_CREATE_VIEW,
DEFAULT_AUTO_CREATE_VIEW,
DOMAIN,
)

from .const import DOMAIN
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry


class ZoneMapperConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
Expand All @@ -22,7 +33,6 @@ async def async_step_user(
self, user_input: dict | None = None
) -> config_entries.ConfigFlowResult:
"""Show a confirmation form and create the entry when submitted."""
# Prevent multiple entries; this integration is singleton.
if self._async_current_entries():
return self.async_abort(reason="already_configured")

Expand All @@ -31,5 +41,37 @@ async def async_step_user(
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Zone Mapper", data={})

# Show a confirmation form with no fields
return self.async_show_form(step_id="user", data_schema=vol.Schema({}))

@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> ZoneMapperOptionsFlow:
"""Return the options flow for this entry."""
return ZoneMapperOptionsFlow(config_entry)


class ZoneMapperOptionsFlow(config_entries.OptionsFlow):
"""Options flow with a single toggle for auto-view seeding."""

def __init__(self, config_entry: ConfigEntry) -> None:
"""Store the entry the options apply to."""
self._config_entry = config_entry

async def async_step_init(
self, user_input: dict | None = None
) -> config_entries.ConfigFlowResult:
"""Prompt for the auto-view toggle and save it."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

current = self._config_entry.options.get(
CONF_AUTO_CREATE_VIEW, DEFAULT_AUTO_CREATE_VIEW
)
schema = vol.Schema(
{
vol.Required(CONF_AUTO_CREATE_VIEW, default=current): bool,
}
)
return self.async_show_form(step_id="init", data_schema=schema)
10 changes: 10 additions & 0 deletions custom_components/zone_mapper/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@
COORD_SENSOR_UNIQUE_ID_FMT = "zone_mapper_{location}_zone_{zone_id}"
PRESENCE_SENSOR_UNIQUE_ID_FMT = "zone_mapper_{location}_zone_{zone_id}_presence"

# Options flow / auto-view seeding
CONF_AUTO_CREATE_VIEW = "auto_create_view"
DEFAULT_AUTO_CREATE_VIEW = True
SEEDED_DEFAULT_VIEW_FLAG = "seeded_default_view"
AUTO_VIEW_TITLE = "Zone Mapper"
AUTO_VIEW_PATH = "zone-mapper"
AUTO_VIEW_ICON = "mdi:map-marker-radius"
AUTO_VIEW_PLACEHOLDER_LOCATION = "Home"
CARD_TYPE = "custom:zone-mapper-card"

# Log / warning templates
WARN_POLY_INSUFFICIENT = (
"Polygon zone %s in location '%s' has insufficient points (<3); clearing zone."
Expand Down
Loading