Skip to content
140 changes: 3 additions & 137 deletions custom_components/geozones/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@
"""The GeoZones Component initialization runtime orchestration module."""

import logging
import os
from datetime import datetime
from typing import Any

import aiofiles # type: ignore[import-untyped]
import voluptuous as vol
from homeassistant.components.frontend import (
async_register_built_in_panel,
async_remove_panel,
)
from homeassistant.components.lovelace.dashboard import LovelaceYAML
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall
Expand All @@ -29,6 +22,7 @@
DEFAULT_USE_CUSTOM_ZONES,
DOMAIN,
)
from .dashboard import async_remove_dashboard, async_setup_dashboard
from .utils import (
async_add_custom_zone,
async_ensure_custom_zones_file,
Expand All @@ -45,7 +39,6 @@
]

CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
DASHBOARD_REL_PATH = "custom_components/geozones/geozones_dashboard.yaml"


def _get_active_select_zone_name(hass: HomeAssistant) -> str | None:
Expand Down Expand Up @@ -75,112 +68,6 @@ async def _async_reprocess_all_entries(hass: HomeAssistant) -> None:
async_dispatcher_send(hass, f"{DOMAIN}_reload_{entry.entry_id}")


async def _async_generate_dashboard_yaml(hass: HomeAssistant) -> str:
"""Generate and write the Lovelace dashboard YAML configuration file."""
dashboard_path = hass.config.path(DASHBOARD_REL_PATH)
os.makedirs(os.path.dirname(dashboard_path), exist_ok=True)

entities_yaml_lines: list[str] = []
entries = hass.config_entries.async_entries(DOMAIN)

if entries:
first_entry = entries[0]
source_tracker = first_entry.data.get(CONF_SOURCE_TRACKER, "")
if source_tracker:
slug = source_tracker.split(".")[-1]
entities_yaml_lines.extend(
[
(
f" - entity: select.geozones_{slug}_custom_zones\n"
" name: Select Custom Zone"
),
(
f" - entity: button.geozones_{slug}_mark_location\n"
" name: Mark Current Location"
),
(
f" - entity: button.geozones_{slug}_remove_zone\n"
" name: Remove Selected Zone"
),
(
f" - entity: button.geozones_{slug}_reload\n"
" name: Reload Layer"
),
]
)

if not entities_yaml_lines:
entities_block = " - entity: device_tracker.geozones"
else:
entities_block = "\n".join(entities_yaml_lines)

yaml_content = f"""title: GeoZones
views:
- title: GeoZonesOverview
path: geozones-overview
icon: mdi:map-marker-radius
type: masonry
cards:
- type: markdown
title: "📍 Active Tracking Overview"
content: |
{{%- set trackers = states.device_tracker | selectattr('entity_id', 'search', '^device_tracker\\\\.geozones_') | list -%}}
{{%- if trackers | length > 0 -%}}
{{%- for t in trackers %}}
### 📱 {{{{ t.name }}}}
* **Current Zone:** `{{{{ t.state }}}}`
* **Source Target:** `{{{{ state_attr(t.entity_id, 'source_entity_id') or t.entity_id }}}}`

**Active inside zones:**
{{%- set zones = state_attr(t.entity_id, 'containing_zones') -%}}
{{%- if zones and zones | length > 0 -%}}
{{%- for zone in zones %}}
- {{{{ zone }}}}
{{%- endfor -%}}
{{%- else %}}
*Not inside any custom zones.*
{{%- endif %}}

{{%- if not loop.last %}}
---
{{%- endif %}}
{{%- endfor -%}}
{{%- else %}}
*No active GeoZones trackers detected.*
{{%- endif -%}}

- type: entities
title: "⚙️ Custom Zone Manager"
show_header_toggle: false
entities:
{entities_block}

- type: markdown
title: "🗺️ All Loaded Zones"
content: |
{{%- set ns = namespace(zones=[]) -%}}
{{%- set trackers = states.device_tracker | selectattr('entity_id', 'search', '^device_tracker\\\\.geozones_') | list -%}}
{{%- for t in trackers -%}}
{{%- set cz = state_attr(t.entity_id, 'containing_zones') or [] -%}}
{{%- set lz = state_attr(t.entity_id, 'loaded_zones') or state_attr(t.entity_id, 'all_zones') or state_attr(t.entity_id, 'zones') or [] -%}}
{{%- set ns.zones = ns.zones + cz + lz -%}}
{{%- endfor -%}}
{{%- set unique_zones = ns.zones | unique | sort -%}}
{{%- if unique_zones | length > 0 -%}}
{{%- for z in unique_zones %}}
- {{{{ z }}}}
{{%- endfor -%}}
{{%- else %}}
*No loaded zones detected across active trackers.*
{{%- endif -%}}
"""

async with aiofiles.open(dashboard_path, mode="w", encoding="utf-8") as file:
await file.write(yaml_content)

return DASHBOARD_REL_PATH


async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the GeoZones component domain and register custom actions."""
await async_ensure_custom_zones_file(hass)
Expand Down Expand Up @@ -317,26 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not hass.data[DOMAIN].get("panel_registered"):
hass.data[DOMAIN]["panel_registered"] = True
try:
rel_path = await _async_generate_dashboard_yaml(hass)

if "lovelace" in hass.data and hasattr(hass.data["lovelace"], "dashboards"):
hass.data["lovelace"].dashboards["geozones"] = LovelaceYAML(
hass, "geozones", {"mode": "yaml", "filename": rel_path}
)

async_register_built_in_panel(
hass,
component_name="lovelace",
sidebar_title="GeoZones",
sidebar_icon="mdi:map-marker-path",
frontend_url_path="geozones",
config={
"mode": "yaml",
"title": "GeoZones",
"icon": "mdi:map-marker-path",
},
require_admin=False,
)
await async_setup_dashboard(hass)
except ValueError:
_LOGGER.debug("GeoZones panel already registered")
except OSError as err:
Expand Down Expand Up @@ -384,9 +252,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if e.entry_id != entry.entry_id
]
if not remaining_entries and hass.data[DOMAIN].get("panel_registered"):
async_remove_panel(hass, "geozones")
if "lovelace" in hass.data and hasattr(hass.data["lovelace"], "dashboards"):
hass.data["lovelace"].dashboards.pop("geozones", None)
await async_remove_dashboard(hass)
hass.data[DOMAIN]["panel_registered"] = False

return unload_ok
Expand Down
186 changes: 186 additions & 0 deletions custom_components/geozones/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# custom_components/geozones/dashboard.py
"""Lovelace dashboard orchestration module for GeoZones."""

import logging
import os

import aiofiles # type: ignore[import-untyped]
from homeassistant.components.frontend import (
async_register_built_in_panel,
async_remove_panel,
)
from homeassistant.components.http import StaticPathConfig
from homeassistant.components.lovelace.dashboard import LovelaceYAML
from homeassistant.core import HomeAssistant

from .const import CONF_SOURCE_TRACKER, DOMAIN

_LOGGER = logging.getLogger(__name__)
DASHBOARD_REL_PATH = "custom_components/geozones/geozones_dashboard.yaml"
LOCAL_LOGO_URL = "/geozones_static/logo.png"


async def async_generate_dashboard_yaml(hass: HomeAssistant) -> str:
"""Generate and write the Lovelace dashboard YAML configuration file."""
dashboard_path = hass.config.path(DASHBOARD_REL_PATH)
os.makedirs(os.path.dirname(dashboard_path), exist_ok=True)

entities_yaml_lines: list[str] = []
entries = hass.config_entries.async_entries(DOMAIN)

if entries:
first_entry = entries[0]
first_tracker = first_entry.data.get(CONF_SOURCE_TRACKER, "")

if first_tracker:
first_slug = first_tracker.split(".")[-1]
entities_yaml_lines.append(
f" - entity: select.geozones_{first_slug}_custom_zones\n"
" name: Select Custom Zone"
)
entities_yaml_lines.append(" - type: divider")

for entry in entries:
source_tracker = entry.data.get(CONF_SOURCE_TRACKER, "")
if not source_tracker:
continue
slug = source_tracker.split(".")[-1]
entities_yaml_lines.append(
f" - entity: button.geozones_{slug}_mark_location\n"
f" name: Mark Current Location for {slug}"
)

if first_tracker:
first_slug = first_tracker.split(".")[-1]
entities_yaml_lines.extend(
[
" - type: divider",
(
f" - entity: button.geozones_{first_slug}_remove_zone\n"
" name: Remove Selected Zone"
),
(
f" - entity: button.geozones_{first_slug}_reload\n"
" name: Reload Layer"
),
]
)

if not entities_yaml_lines:
entities_block = " - entity: device_tracker.geozones"
else:
entities_block = "\n".join(entities_yaml_lines)

yaml_content = f"""title: GeoZones
views:
- title: GeoZones
path: geozones-overview
icon: mdi:map-marker-radius
type: masonry
cards:
- type: markdown
title: "📍 Active Tracking Overview"
content: |
<div align="center" style="margin-bottom: 16px;">
<img src="{LOCAL_LOGO_URL}" width="130" alt="GeoZones Logo">
</div>

{{%- set trackers = states.device_tracker | selectattr('entity_id', 'search', '^device_tracker\\\\.geozones_') | list -%}}
{{%- if trackers | length > 0 -%}}
{{%- for t in trackers %}}
### 📱 {{{{ t.name }}}}
* **Current Zone:** `{{{{ t.state }}}}`
* **Source Target:** `{{{{ state_attr(t.entity_id, 'source_entity_id') or t.entity_id }}}}`

**Active inside zones:**
{{%- set zones = state_attr(t.entity_id, 'containing_zones') -%}}
{{%- if zones and zones | length > 0 -%}}
{{%- for zone in zones %}}
- {{{{ zone }}}}
{{%- endfor -%}}
{{%- else %}}
*Not inside any custom zones.*
{{%- endif %}}

{{%- if not loop.last %}}
---
{{%- endif %}}
{{%- endfor -%}}
{{%- else %}}
*No active GeoZones trackers detected.*
{{%- endif -%}}

- type: entities
title: "⚙️ Custom Zone Manager"
show_header_toggle: false
entities:
{entities_block}

- type: markdown
title: "🗺️ All Available Zones"
content: |
{{%- set ns = namespace(zones=[]) -%}}
{{%- set trackers = states.device_tracker | selectattr('entity_id', 'search', '^device_tracker\\\\.geozones_') | list -%}}
{{%- for t in trackers -%}}
{{%- set lz = state_attr(t.entity_id, 'loaded_zones') or state_attr(t.entity_id, 'available_zones') or state_attr(t.entity_id, 'all_zones') or state_attr(t.entity_id, 'zones') or [] -%}}
{{%- set ns.zones = ns.zones + lz -%}}
{{%- endfor -%}}
{{%- set unique_zones = ns.zones | unique | sort -%}}
{{%- if unique_zones | length > 0 -%}}
Total loaded zones across all active files: **{{{{ unique_zones | length }}}}**

{{%- for z in unique_zones %}}
- {{{{ z }}}}
{{%- endfor -%}}
{{%- else %}}
*No loaded zones available across active trackers.*
{{%- endif -%}}
"""

async with aiofiles.open(dashboard_path, mode="w", encoding="utf-8") as file:
await file.write(yaml_content)

return DASHBOARD_REL_PATH


async def async_setup_dashboard(hass: HomeAssistant) -> None:
"""Set up and register the GeoZones sidebar panel dashboard."""
brand_dir = hass.config.path("custom_components/geozones/brand")
if os.path.exists(brand_dir):
await hass.http.async_register_static_paths(
[
StaticPathConfig(
url_path="/geozones_static",
path=brand_dir,
cache_headers=True,
)
]
)

rel_path = await async_generate_dashboard_yaml(hass)

if "lovelace" in hass.data and hasattr(hass.data["lovelace"], "dashboards"):
hass.data["lovelace"].dashboards["geozones"] = LovelaceYAML(
hass, "geozones", {"mode": "yaml", "filename": rel_path}
)

async_register_built_in_panel(
hass,
component_name="lovelace",
sidebar_title="GeoZones",
sidebar_icon="mdi:map-marker-path",
frontend_url_path="geozones",
config={
"mode": "yaml",
"title": "GeoZones",
"icon": "mdi:map-marker-path",
},
require_admin=False,
)


async def async_remove_dashboard(hass: HomeAssistant) -> None:
"""Remove the GeoZones sidebar panel dashboard."""
async_remove_panel(hass, "geozones")
if "lovelace" in hass.data and hasattr(hass.data["lovelace"], "dashboards"):
hass.data["lovelace"].dashboards.pop("geozones", None)
2 changes: 1 addition & 1 deletion custom_components/geozones/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "geozones",
"name": "GeoZones",
"after_dependencies": ["lovelace"],
"after_dependencies": ["http","lovelace"],
"codeowners": ["@ticstyle"],
"config_flow": true,
"documentation": "https://github.com/ticstyle/GeoZones",
Expand Down
Loading