Files
HomeAssistantVS/custom_components/spook/ectoplasms/zone/services/update.py
T

119 lines
4.4 KiB
Python

"""Spook - Your homie."""
from __future__ import annotations
from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.zone import (
DOMAIN,
ENTITY_ID_HOME,
UPDATE_FIELDS,
Zone,
ZoneStorageCollection,
)
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
from homeassistant.core import ServiceCall
def _is_the_house(entity: Zone) -> bool:
"""Tell whether a zone is the home zone drawn from the house's location.
Home Assistant only draws it when no zone has taken `zone.home` already:
zones from YAML and storage are set up first, and one named Home gets that
entity ID. Those are ordinary zones, and moving the house would not move
them. The drawn one is the only editable zone without a stored ID.
"""
return (
entity.entity_id == ENTITY_ID_HOME
and entity.editable
# pylint: disable-next=protected-access
and "id" not in entity._config # noqa: SLF001
)
class SpookService(AbstractSpookAdminService):
"""Zone service to update a zone on the fly, the home zone included."""
domain = DOMAIN
service = "update"
schema = {
vol.Required("entity_id"): cv.entity_domain(DOMAIN),
} | UPDATE_FIELDS
async def async_handle_service(self, call: ServiceCall) -> None:
"""Handle the service call."""
entity_component: EntityComponent[Zone] = self.hass.data[DATA_INSTANCES][DOMAIN]
collection: ZoneStorageCollection
if DOMAIN in self.hass.data:
collection = self.hass.data[DOMAIN]
else:
# Home zone is set in YAML, as a result Home Assistant doesn't
# set the storage collection into hass data.
# Major hack to get around this. 👻
collection = self.hass.data["websocket_api"]["zone/list"][
0
].__self__.storage_collection
if not (entity := entity_component.get_entity(call.data["entity_id"])):
message = f"Could not find entity_id: {call.data['entity_id']}"
raise HomeAssistantError(message)
if _is_the_house(entity):
await self._async_update_home(call)
return
# pylint: disable-next=protected-access
if not entity.editable or "id" not in entity._config: # noqa: SLF001
message = f"This zone is not editable: {call.data['entity_id']}"
raise HomeAssistantError(message)
data = call.data.copy()
data.pop("entity_id")
# pylint: disable-next=protected-access
await collection.async_update_item(entity._config["id"], data) # noqa: SLF001
async def _async_update_home(self, call: ServiceCall) -> None:
"""Move or resize the home zone.
It is not a stored zone: Home Assistant draws it from the location
set for the whole house, and redraws it when that changes. So this
changes the location, the same way Home Assistant's own
`homeassistant.set_location` does, and the radius along with it.
Only those. Its name is the name of the whole house, and a home zone
is never passive.
"""
data = dict(call.data)
data.pop("entity_id")
if unsupported := set(data) - {CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS}:
message = (
"The home zone only takes a latitude, longitude and radius, "
f"not: {', '.join(sorted(unsupported))}"
)
raise HomeAssistantError(message)
# Home Assistant keeps the radius of the house in whole meters, and
# never below zero. Cutting a fraction off quietly would be a
# different zone than was asked for, and the location is stored
# without going through the checks its configuration normally does.
if CONF_RADIUS in data:
radius = data[CONF_RADIUS]
if radius != int(radius) or radius < 0:
message = "The home zone takes its radius in whole meters, zero or more"
raise HomeAssistantError(message)
data[CONF_RADIUS] = int(radius)
await self.hass.config.async_update(**data)