221 files

This commit is contained in:
Home Assistant Version Control
2026-10-10 15:24:58 +00:00
parent ae12b88c7d
commit bfafb4c28a
221 changed files with 73725 additions and 3277 deletions
@@ -14,6 +14,7 @@ from homeassistant.components.blueprint.importer import fetch_blueprint_from_url
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -37,17 +38,26 @@ class SpookService(AbstractSpookAdminService):
call.data["url"],
)
except (TimeoutError, aiohttp.ClientError) as err:
msg = "Error fetching blueprint from URL"
raise HomeAssistantError(msg) from err
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="blueprint_fetch_failed",
) from err
if imported_blueprint is None:
msg = "This url is not supported"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="blueprint_url_not_supported",
)
domain_blueprints: dict[str, DomainBlueprints] = self.hass.data.get(DOMAIN, {})
if imported_blueprint.blueprint.domain not in domain_blueprints:
msg = f"Unsupported domain: {imported_blueprint.blueprint.domain}"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="blueprint_domain_not_supported",
translation_placeholders={
"domain": imported_blueprint.blueprint.domain,
},
)
imported_blueprint.blueprint.update_metadata(source_url=call.data["url"])
@@ -59,8 +69,12 @@ class SpookService(AbstractSpookAdminService):
imported_blueprint.suggested_filename,
)
except FileAlreadyExists as ex:
msg = "File already exists"
raise HomeAssistantError(msg) from ex
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="blueprint_file_exists",
) from ex
except OSError as err:
msg = "Error writing file"
raise HomeAssistantError(msg) from err
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="blueprint_write_failed",
) from err
@@ -7,11 +7,11 @@ from homeassistant.const import (
EVENT_COMPONENT_LOADED,
)
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import DATA_ENTITY_PLATFORM, EntityPlatform
from ....const import LOGGER
from ....entity_filtering import async_filter_known_entity_ids, async_get_all_entity_ids
from ....entity_suggestions import async_describe_unknown_entities
from ....helper_sources import async_group_members
from ....repairs import AbstractSpookRepair
@@ -35,55 +35,35 @@ class SpookRepair(AbstractSpookRepair):
known_entity_ids = async_get_all_entity_ids(self.hass)
platforms: list[EntityPlatform] | None
if not (platforms := self.hass.data[DATA_ENTITY_PLATFORM].get(self.domain)):
return # Nothing to do.
for entity, members in async_group_members(self.hass):
self.possible_issue_ids.add(entity.entity_id)
# Taken as a snapshot: describing what is missing can hand the event
# loop a turn, and a change to the live collection during it ends the
# inspection in a `RuntimeError`. #1558.
for platform in list(platforms):
# We don't want to check the old style group platform
for entity in list(platform.entities.values()):
self.possible_issue_ids.add(entity.entity_id)
members = []
if platform.domain == group.DOMAIN:
members = entity.tracking
elif hasattr(entity, "_entity_ids"):
# pylint: disable-next=protected-access
members = entity._entity_ids # noqa: SLF001
elif hasattr(entity, "_entities"):
# pylint: disable-next=protected-access
members = entity._entities # noqa: SLF001
if unknown_entities := async_filter_known_entity_ids(
self.hass, entity_ids=members, known_entity_ids=known_entity_ids
):
described = await async_describe_unknown_entities(
self.hass, sorted(unknown_entities)
)
self.async_create_issue(
issue_id=entity.entity_id,
references=unknown_entities,
is_fixable=True,
data={
"group_entity_id": entity.entity_id,
# What the fix checks again before dropping any.
"group_unknown_entity_ids": ",".join(
sorted(unknown_entities)
),
"group": entity.name,
"entities": described,
},
translation_placeholders={
"entities": described,
"group": entity.name,
"entity_id": entity.entity_id,
},
)
LOGGER.debug(
"Spook found unknown member entities in %s "
"and created an issue for it; Entities: %s",
entity.entity_id,
", ".join(unknown_entities),
)
if unknown_entities := async_filter_known_entity_ids(
self.hass, entity_ids=members, known_entity_ids=known_entity_ids
):
described = await async_describe_unknown_entities(
self.hass, sorted(unknown_entities)
)
self.async_create_issue(
issue_id=entity.entity_id,
references=unknown_entities,
is_fixable=True,
data={
"group_entity_id": entity.entity_id,
# What the fix checks again before dropping any.
"group_unknown_entity_ids": ",".join(sorted(unknown_entities)),
"group": entity.name,
"entities": described,
},
translation_placeholders={
"entities": described,
"group": entity.name,
"entity_id": entity.entity_id,
},
)
LOGGER.debug(
"Spook found unknown member entities in %s "
"and created an issue for it; Entities: %s",
entity.entity_id,
", ".join(unknown_entities),
)
@@ -3,8 +3,8 @@
from __future__ import annotations
from homeassistant.components import homeassistant
from homeassistant.const import __version__
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.loader import async_get_loaded_integration
from ...const import DOMAIN
from ...entity import SpookEntity, SpookEntityDescription
@@ -16,10 +16,21 @@ class HomeAssistantSpookEntity(SpookEntity):
def __init__(self, description: SpookEntityDescription) -> None:
"""Initialize the entity."""
super().__init__(description=description)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, homeassistant.DOMAIN)},
manufacturer="Home Assistant",
name="Home Assistant",
sw_version=__version__,
)
self._attr_unique_id = f"{homeassistant.DOMAIN}_{description.key}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device, which is Spook's, so it runs Spook's version.
Read once the entity is added to Home Assistant, as the version comes
from the loaded integration.
"""
version = async_get_loaded_integration(self.hass, DOMAIN).version
return DeviceInfo(
identifiers={(DOMAIN, homeassistant.DOMAIN)},
manufacturer="Spook 👻",
name="Home Assistant",
sw_version=str(version) if version else None,
configuration_url="https://spook.boo/devices-entities",
entry_type=DeviceEntryType.SERVICE,
)
@@ -13,9 +13,12 @@ from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from ....const import DOMAIN
from ....errors import entity_not_found
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant, ServiceCall
@@ -41,13 +44,20 @@ def async_config_entry_ids(hass: HomeAssistant, call: ServiceCall) -> list[str]:
domains: list[str] = call.data.get(CONF_DOMAIN, [])
if not entry_ids and not domains:
msg = "Name the integration entries to change, or their integration"
raise ServiceValidationError(msg)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_entries_required",
)
for domain in domains:
if not (entries := hass.config_entries.async_entries(domain)):
msg = f"The {domain} integration has no entries"
raise ServiceValidationError(msg)
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_without_entries",
translation_placeholders={
"domain": domain,
},
)
entry_ids.extend(
entry.entry_id for entry in entries if entry.entry_id not in entry_ids
)
@@ -82,8 +92,7 @@ def async_set_voice_assistant_exposure(
hass.states.get(entity_id) is None
and entity_registry.async_get(entity_id) is None
):
msg = f"Unknown entity: {entity_id}"
raise HomeAssistantError(msg)
raise entity_not_found(entity_id)
for entity_id in entity_ids:
for assistant in call.data[CONF_ASSISTANTS]:
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry as ar, config_validation as cv
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
area_registry = ar.async_get(self.hass)
if not (area := area_registry.async_get_area(call.data["area_id"])):
msg = f"Area {call.data['area_id']} not found"
raise HomeAssistantError(msg)
raise area_not_found(call.data["area_id"])
aliases = area.aliases.copy()
area_registry.async_update(
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
floor_registry as fr,
)
from ....errors import floor_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -33,8 +33,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
floor_registry = fr.async_get(self.hass)
if not (floor := floor_registry.async_get_floor(call.data["floor_id"])):
msg = f"Floor {call.data['floor_id']} not found"
raise HomeAssistantError(msg)
raise floor_not_found(call.data["floor_id"])
aliases = floor.aliases.copy()
floor_registry.async_update(
@@ -7,13 +7,13 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
floor_registry as fr,
)
from ....errors import floor_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -34,8 +34,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
floor_registry = fr.async_get(self.hass)
if not floor_registry.async_get_floor(call.data["floor_id"]):
msg = f"Floor {call.data['floor_id']} not found"
raise HomeAssistantError(msg)
raise floor_not_found(call.data["floor_id"])
area_registry = ar.async_get(self.hass)
for area_id in call.data["area_id"]:
@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
@@ -15,6 +14,7 @@ from homeassistant.helpers import (
)
from ....core_compat import async_update_any_device
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -35,8 +35,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
area_registry = ar.async_get(self.hass)
if not area_registry.async_get_area(call.data["area_id"]):
msg = f"Area {call.data['area_id']} not found"
raise HomeAssistantError(msg)
raise area_not_found(call.data["area_id"])
device_registry = dr.async_get(self.hass)
for device_id in call.data["device_id"]:
@@ -7,13 +7,13 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
entity_registry as er,
)
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -34,8 +34,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
area_registry = ar.async_get(self.hass)
if not area_registry.async_get_area(call.data["area_id"]):
msg = f"Area {call.data['area_id']} not found"
raise HomeAssistantError(msg)
raise area_not_found(call.data["area_id"])
entity_registry = er.async_get(self.hass)
for entity_id in call.data["entity_id"]:
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
)
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -41,8 +41,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for area_id in call.data["area_id"]:
if (area_entry := area_registry.async_get_area(area_id)) is None:
msg = f"Area {area_id} not found"
raise HomeAssistantError(msg)
raise area_not_found(area_id)
labels = area_entry.labels.copy()
labels.update(call.data["label_id"])
@@ -7,13 +7,13 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
)
from ....core_compat import async_update_any_device
from ....errors import device_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -42,8 +42,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for device_id in call.data["device_id"]:
if (device_entry := device_registry.async_get(device_id)) is None:
msg = f"Device {device_id} not found"
raise HomeAssistantError(msg)
raise device_not_found(device_id)
labels = device_entry.labels.copy()
labels.update(call.data["label_id"])
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
)
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -41,8 +41,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for entity_id in call.data["entity_id"]:
if (entity_entry := entity_registry.async_get(entity_id)) is None:
msg = f"Entity {entity_id} not found"
raise HomeAssistantError(msg)
raise entity_not_found(entity_id)
labels = entity_entry.labels.copy()
labels.update(call.data["label_id"])
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....errors import config_entry_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
call.data["config_entry_id"],
)
):
msg = f"Config entry not found: {call.data['config_entry_id']}"
raise HomeAssistantError(msg)
raise config_entry_not_found(call.data["config_entry_id"])
self.hass.config_entries.async_update_entry(
entry,
@@ -10,6 +10,8 @@ from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import user_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -31,11 +33,15 @@ class SpookService(AbstractSpookAdminService):
for user_id in call.data["user_id"]:
user = await self.hass.auth.async_get_user(user_id)
if user is None:
message = f"Could not find user: {user_id}"
raise HomeAssistantError(message)
raise user_not_found(user_id)
if user.system_generated:
message = f"Cannot disable a system-generated user: {user_id}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="cannot_disable_system_user",
translation_placeholders={
"user_id": user_id,
},
)
users.append(user)
for user in users:
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....errors import config_entry_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
call.data["config_entry_id"],
)
):
msg = f"Config entry not found: {call.data['config_entry_id']}"
raise HomeAssistantError(msg)
raise config_entry_not_found(call.data["config_entry_id"])
self.hass.config_entries.async_update_entry(
entry,
@@ -10,6 +10,8 @@ from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import user_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -31,11 +33,15 @@ class SpookService(AbstractSpookAdminService):
for user_id in call.data["user_id"]:
user = await self.hass.auth.async_get_user(user_id)
if user is None:
message = f"Could not find user: {user_id}"
raise HomeAssistantError(message)
raise user_not_found(user_id)
if user.system_generated:
message = f"Cannot enable a system-generated user: {user_id}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="cannot_enable_system_user",
translation_placeholders={
"user_id": user_id,
},
)
users.append(user)
for user in users:
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry as ar, config_validation as cv
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
area_registry = ar.async_get(self.hass)
if not (area := area_registry.async_get_area(call.data["area_id"])):
msg = f"Area {call.data['area_id']} not found"
raise HomeAssistantError(msg)
raise area_not_found(call.data["area_id"])
aliases = area.aliases.copy()
area_registry.async_update(
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, floor_registry as fr
from ....errors import floor_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
floor_registry = fr.async_get(self.hass)
if not (floor := floor_registry.async_get_floor(call.data["floor_id"])):
msg = f"Floor {call.data['floor_id']} not found"
raise HomeAssistantError(msg)
raise floor_not_found(call.data["floor_id"])
aliases = floor.aliases.copy()
floor_registry.async_update(
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
area_registry as ar,
config_validation as cv,
)
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -41,8 +41,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for area_id in call.data["area_id"]:
if (area_entry := area_registry.async_get_area(area_id)) is None:
msg = f"Area {area_id} not found"
raise HomeAssistantError(msg)
raise area_not_found(area_id)
labels = area_entry.labels.copy()
labels.difference_update(call.data["label_id"])
@@ -7,13 +7,13 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
)
from ....core_compat import async_update_any_device
from ....errors import device_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -42,8 +42,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for device_id in call.data["device_id"]:
if (device_entry := device_registry.async_get(device_id)) is None:
msg = f"Device {device_id} not found"
raise HomeAssistantError(msg)
raise device_not_found(device_id)
labels = device_entry.labels.copy()
labels.difference_update(call.data["label_id"])
@@ -7,12 +7,12 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
entity_registry as er,
)
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
from ..labels import async_check_labels_exist
@@ -41,8 +41,7 @@ class SpookService(AbstractSpookAdminService):
updates: dict[str, set[str]] = {}
for entity_id in call.data["entity_id"]:
if (entity_entry := entity_registry.async_get(entity_id)) is None:
msg = f"Entity {entity_id} not found"
raise HomeAssistantError(msg)
raise entity_not_found(entity_id)
labels = entity_entry.labels.copy()
labels.difference_update(call.data["label_id"])
@@ -12,7 +12,7 @@ from homeassistant.const import RESTART_EXIT_CODE
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import LOGGER
from ....const import DOMAIN as SPOOK_DOMAIN, LOGGER
from ....services import AbstractSpookAdminService, ReplaceExistingService
if TYPE_CHECKING:
@@ -42,9 +42,9 @@ class SpookService(AbstractSpookAdminService, ReplaceExistingService):
return
if not self.overriden_service:
msg = "Spook encountered an error while restarting Home Assistant."
raise HomeAssistantError(
msg,
translation_domain=SPOOK_DOMAIN,
translation_key="restart_failed",
)
# Waited on, the way calling Home Assistant's own restart is. It
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry as ar, config_validation as cv
from ....errors import area_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
area_registry = ar.async_get(self.hass)
if not area_registry.async_get_area(call.data["area_id"]):
msg = f"Area {call.data['area_id']} not found"
raise HomeAssistantError(msg)
raise area_not_found(call.data["area_id"])
area_registry.async_update(
call.data["area_id"],
@@ -7,9 +7,9 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.components.homeassistant import DOMAIN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, floor_registry as fr
from ....errors import floor_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -30,8 +30,7 @@ class SpookService(AbstractSpookAdminService):
"""Handle the service call."""
floor_registry = fr.async_get(self.hass)
if not floor_registry.async_get_floor(call.data["floor_id"]):
msg = f"Floor {call.data['floor_id']} not found"
raise HomeAssistantError(msg)
raise floor_not_found(call.data["floor_id"])
floor_registry.async_update(
call.data["floor_id"],
@@ -10,6 +10,8 @@ import voluptuous as vol
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN
if TYPE_CHECKING:
from homeassistant.components.input_number import InputNumber
from homeassistant.core import ServiceCall
@@ -31,8 +33,14 @@ def native_value_as_float(entity: InputNumber) -> float:
try:
return float(entity.native_value)
except (TypeError, ValueError) as err:
msg = f"Value {entity.native_value!r} for {entity.entity_id} is not a number"
raise HomeAssistantError(msg) from err
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="value_not_a_number",
translation_placeholders={
"value": repr(entity.native_value),
"entity_id": entity.entity_id,
},
) from err
def step_amount(entity: InputNumber, call: ServiceCall) -> float:
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
from homeassistant.exceptions import HomeAssistantError
from ....const import DOMAIN
if TYPE_CHECKING:
from homeassistant.components.number import NumberEntity
@@ -21,8 +23,14 @@ def shown_value_as_float(entity: NumberEntity) -> float:
try:
return float(entity.value)
except (TypeError, ValueError) as err:
msg = f"Value {entity.value!r} for {entity.entity_id} is not a number"
raise HomeAssistantError(msg) from err
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="value_not_a_number",
translation_placeholders={
"value": repr(entity.value),
"entity_id": entity.entity_id,
},
) from err
async def async_set_shown_value(entity: NumberEntity, value: float) -> None:
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
from homeassistant.components.number import DOMAIN, NumberEntity
from homeassistant.exceptions import HomeAssistantError
from ....const import DOMAIN as SPOOK_DOMAIN
from ....services import AbstractSpookEntityComponentService
if TYPE_CHECKING:
@@ -26,6 +27,11 @@ class SpookService(AbstractSpookEntityComponentService[NumberEntity]):
) -> None:
"""Handle the service call."""
if entity.max_value is None:
msg = f"Entity {entity.entity_id} has no max value"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="no_max_value",
translation_placeholders={
"entity_id": entity.entity_id,
},
)
await entity.async_set_native_value(entity.native_max_value)
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
from homeassistant.components.number import DOMAIN, NumberEntity
from homeassistant.exceptions import HomeAssistantError
from ....const import DOMAIN as SPOOK_DOMAIN
from ....services import AbstractSpookEntityComponentService
if TYPE_CHECKING:
@@ -26,6 +27,11 @@ class SpookService(AbstractSpookEntityComponentService[NumberEntity]):
) -> None:
"""Handle the service call."""
if entity.min_value is None:
msg = f"Entity {entity.entity_id} has no min value"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="no_min_value",
translation_placeholders={
"entity_id": entity.entity_id,
},
)
await entity.async_set_native_value(entity.native_min_value)
@@ -11,6 +11,8 @@ from homeassistant.components.person import DOMAIN, Person, PersonStorageCollect
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -38,13 +40,17 @@ class SpookService(AbstractSpookAdminService):
_, collection, entity_component = self.hass.data[DOMAIN]
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)
raise entity_not_found(call.data["entity_id"])
# pylint: disable-next=protected-access
if not entity.editable or "id" not in entity._config: # noqa: SLF001
message = f"This person is not editable: {call.data['entity_id']}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="person_not_editable",
translation_placeholders={
"entity_id": call.data["entity_id"],
},
)
await collection.async_update_item(
# pylint: disable-next=protected-access
@@ -11,6 +11,8 @@ from homeassistant.components.person import DOMAIN, Person, PersonStorageCollect
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -38,13 +40,17 @@ class SpookService(AbstractSpookAdminService):
_, collection, entity_component = self.hass.data[DOMAIN]
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)
raise entity_not_found(call.data["entity_id"])
# pylint: disable-next=protected-access
if not entity.editable or "id" not in entity._config: # noqa: SLF001
message = f"This person is not editable: {call.data['entity_id']}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="person_not_editable",
translation_placeholders={
"entity_id": call.data["entity_id"],
},
)
await collection.async_update_item(
# pylint: disable-next=protected-access
@@ -3,8 +3,8 @@
from __future__ import annotations
from homeassistant.components import repairs
from homeassistant.const import __version__
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.loader import async_get_loaded_integration
from ...const import DOMAIN
from ...entity import SpookEntity, SpookEntityDescription
@@ -16,10 +16,21 @@ class RepairsSpookEntity(SpookEntity):
def __init__(self, description: SpookEntityDescription) -> None:
"""Initialize the entity."""
super().__init__(description=description)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, repairs.DOMAIN)},
manufacturer="Home Assistant",
name="Repairs",
sw_version=__version__,
)
self._attr_unique_id = f"{repairs.DOMAIN}_{description.key}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device, which is Spook's, so it runs Spook's version.
Read once the entity is added to Home Assistant, as the version comes
from the loaded integration.
"""
version = async_get_loaded_integration(self.hass, DOMAIN).version
return DeviceInfo(
identifiers={(DOMAIN, repairs.DOMAIN)},
manufacturer="Spook 👻",
name="Repairs",
sw_version=str(version) if version else None,
configuration_url="https://spook.boo/repairs",
entry_type=DeviceEntryType.SERVICE,
)
@@ -21,5 +21,7 @@ class SpookService(AbstractSpookService):
async def async_handle_service(self, _: ServiceCall) -> None:
"""Handle the service call."""
msg = "Spooked!"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="spooked",
)
@@ -23,5 +23,7 @@ class SpookService(AbstractSpookService):
async def async_handle_service(self, _: ServiceCall) -> None:
"""Handle the service call."""
if random.choice([True, False]): # noqa: S311
msg = "Spooked!"
raise HomeAssistantError(msg)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="spooked",
)
@@ -17,6 +17,7 @@ from homeassistant.const import CONF_ID
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from ....const import DOMAIN as SPOOK_DOMAIN
from ....services import AbstractSpookEntityComponentService
if TYPE_CHECKING:
@@ -45,8 +46,13 @@ class SpookService(AbstractSpookEntityComponentService[Timer]):
entity_id = entity.entity_id
if not entity.editable or not entity.unique_id:
message = f"This timer is not editable: {entity_id}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="timer_not_editable",
translation_placeholders={
"entity_id": entity_id,
},
)
# pylint: disable-next=protected-access
updates = entity._config.copy() # noqa: SLF001
@@ -11,6 +11,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -46,13 +48,17 @@ class SpookService(AbstractSpookAdminService):
zone_ids = []
for entity_id in call.data["entity_id"]:
if not (entity := entity_component.get_entity(entity_id)):
message = f"Could not find entity_id: {entity_id}"
raise HomeAssistantError(message)
raise entity_not_found(entity_id)
# pylint: disable-next=protected-access
if not entity.editable or "id" not in entity._config: # noqa: SLF001
message = f"This zone is not editable: {entity_id}"
raise HomeAssistantError(message)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="zone_not_editable",
translation_placeholders={
"entity_id": entity_id,
},
)
# pylint: disable-next=protected-access
zone_ids.append(entity._config["id"]) # noqa: SLF001
@@ -18,6 +18,8 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import DATA_INSTANCES, EntityComponent
from ....const import DOMAIN as SPOOK_DOMAIN
from ....errors import entity_not_found
from ....services import AbstractSpookAdminService
if TYPE_CHECKING:
@@ -65,8 +67,7 @@ class SpookService(AbstractSpookAdminService):
].__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)
raise entity_not_found(call.data["entity_id"])
if _is_the_house(entity):
await self._async_update_home(call)
@@ -74,8 +75,13 @@ class SpookService(AbstractSpookAdminService):
# 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)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="zone_not_editable",
translation_placeholders={
"entity_id": call.data["entity_id"],
},
)
data = call.data.copy()
data.pop("entity_id")
@@ -98,11 +104,11 @@ class SpookService(AbstractSpookAdminService):
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(
translation_domain=SPOOK_DOMAIN,
translation_key="home_zone_unsupported",
translation_placeholders={"fields": ", ".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
@@ -111,8 +117,10 @@ class SpookService(AbstractSpookAdminService):
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)
raise HomeAssistantError(
translation_domain=SPOOK_DOMAIN,
translation_key="home_zone_radius",
)
data[CONF_RADIUS] = int(radius)
await self.hass.config.async_update(**data)