Updated apps
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.helpers import area_registry as ar
|
||||
|
||||
from ....entity_filtering import async_filter_known_area_ids, async_get_all_area_ids
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced areas in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_area_references"
|
||||
inspect_events = {
|
||||
automation.EVENT_AUTOMATION_RELOADED,
|
||||
ar.EVENT_AREA_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "areas"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_area_ids: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known area IDs for this inspection cycle."""
|
||||
self._known_area_ids = async_get_all_area_ids(self.hass)
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown area IDs referenced by ``entity``."""
|
||||
return async_filter_known_area_ids(
|
||||
self.hass,
|
||||
area_ids=entity.referenced_areas,
|
||||
known_area_ids=self._known_area_ids,
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from ....entity_filtering import async_filter_known_device_ids, async_get_all_device_ids
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
|
||||
def extract_event_data_device_ids_from_trigger_config(
|
||||
config: dict[str, Any] | list,
|
||||
) -> set[str]:
|
||||
"""Extract device IDs from event trigger data."""
|
||||
device_ids = set()
|
||||
|
||||
if not config:
|
||||
return device_ids
|
||||
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
device_ids.update(extract_event_data_device_ids_from_trigger_config(item))
|
||||
return device_ids
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return device_ids
|
||||
|
||||
if config.get("platform", config.get("trigger")) == "event" and isinstance(
|
||||
event_data := config.get("event_data"), dict
|
||||
):
|
||||
value = event_data.get("device_id")
|
||||
if isinstance(value, str):
|
||||
device_ids.add(value)
|
||||
elif isinstance(value, list):
|
||||
device_ids.update(item for item in value if isinstance(item, str))
|
||||
|
||||
for value in config.values():
|
||||
if isinstance(value, (dict, list)):
|
||||
device_ids.update(extract_event_data_device_ids_from_trigger_config(value))
|
||||
|
||||
return device_ids
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced devices in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_device_references"
|
||||
inspect_events = {
|
||||
automation.EVENT_AUTOMATION_RELOADED,
|
||||
dr.EVENT_DEVICE_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "devices"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_device_ids: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known device IDs for this inspection cycle."""
|
||||
self._known_device_ids = async_get_all_device_ids(self.hass)
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown device IDs referenced by ``entity``."""
|
||||
device_ids = set(entity.referenced_devices)
|
||||
|
||||
if hasattr(entity, "raw_config") and entity.raw_config:
|
||||
device_ids.difference_update(
|
||||
extract_event_data_device_ids_from_trigger_config(
|
||||
entity.raw_config.get("trigger")
|
||||
)
|
||||
)
|
||||
device_ids.difference_update(
|
||||
extract_event_data_device_ids_from_trigger_config(
|
||||
entity.raw_config.get("triggers")
|
||||
)
|
||||
)
|
||||
|
||||
return async_filter_known_device_ids(
|
||||
self.hass,
|
||||
device_ids=device_ids,
|
||||
known_device_ids=self._known_device_ids,
|
||||
)
|
||||
@@ -0,0 +1,391 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_STATE_CHANGED
|
||||
from homeassistant.core import Event, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from ....const import LOGGER
|
||||
from ....entity_filtering import (
|
||||
ENTITY_ID_PATTERN,
|
||||
async_extract_entities_from_config,
|
||||
async_extract_entities_from_template_string,
|
||||
async_filter_known_entity_ids_with_templates,
|
||||
async_get_all_entity_ids,
|
||||
is_template_string,
|
||||
)
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def extract_template_entities_from_automation_entity(
|
||||
hass: HomeAssistant, entity: Any
|
||||
) -> set[str]:
|
||||
"""Extract entities from automation configuration using Template analysis.
|
||||
|
||||
This function finds template strings in automation configuration and creates
|
||||
Template objects to extract entity references using Template.async_render_to_info().
|
||||
This provides more comprehensive entity detection than regex-based parsing alone.
|
||||
"""
|
||||
# Get the automation configuration
|
||||
config = None
|
||||
if hasattr(entity, "raw_config") and entity.raw_config:
|
||||
config = entity.raw_config
|
||||
else:
|
||||
return set()
|
||||
|
||||
return await async_extract_entities_from_config(hass, config)
|
||||
|
||||
|
||||
async def extract_entities_from_automation_config(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from automation configuration."""
|
||||
entities = set()
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
# Extract entities from trigger config
|
||||
for key in ("trigger", "triggers"):
|
||||
if key in config:
|
||||
entities.update(
|
||||
await extract_entities_from_trigger_config(hass, config[key])
|
||||
)
|
||||
|
||||
# Extract entities from condition config
|
||||
for key in ("condition", "conditions"):
|
||||
if key in config:
|
||||
entities.update(
|
||||
await extract_entities_from_condition_config(hass, config[key])
|
||||
)
|
||||
|
||||
# Extract entities from action config
|
||||
for key in ("action", "actions"):
|
||||
if key in config:
|
||||
entities.update(
|
||||
await extract_entities_from_action_config(hass, config[key])
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def extract_entities_from_trigger_config(
|
||||
hass: HomeAssistant, config: dict[str, Any] | list
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from trigger configuration."""
|
||||
entities = set()
|
||||
|
||||
if not config:
|
||||
return entities
|
||||
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
entities.update(await extract_entities_from_trigger_config(hass, item))
|
||||
return entities
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
# Entity ID fields in triggers
|
||||
for key in ("entity_id", "device_id"):
|
||||
if key in config:
|
||||
entities.update(await extract_entities_from_value(hass, config[key]))
|
||||
|
||||
# Zone trigger has zone field
|
||||
if "zone" in config:
|
||||
entities.update(await extract_entities_from_value(hass, config["zone"]))
|
||||
|
||||
# Extract from nested configs
|
||||
for value in config.values():
|
||||
if isinstance(value, (dict, list)):
|
||||
entities.update(await extract_entities_from_trigger_config(hass, value))
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def extract_event_types_from_trigger_config(config: dict[str, Any] | list) -> set[str]:
|
||||
"""Extract event types from trigger configuration."""
|
||||
event_types = set()
|
||||
|
||||
if not config:
|
||||
return event_types
|
||||
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
event_types.update(extract_event_types_from_trigger_config(item))
|
||||
return event_types
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return event_types
|
||||
|
||||
value = config.get("event_type")
|
||||
if isinstance(value, str):
|
||||
event_types.add(value)
|
||||
elif isinstance(value, list):
|
||||
event_types.update(item for item in value if isinstance(item, str))
|
||||
|
||||
for value in config.values():
|
||||
if isinstance(value, (dict, list)):
|
||||
event_types.update(extract_event_types_from_trigger_config(value))
|
||||
|
||||
return event_types
|
||||
|
||||
|
||||
async def extract_entities_from_condition_config(
|
||||
hass: HomeAssistant, config: dict[str, Any] | list
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from condition configuration."""
|
||||
entities = set()
|
||||
|
||||
if not config:
|
||||
return entities
|
||||
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
entities.update(await extract_entities_from_condition_config(hass, item))
|
||||
return entities
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
# Entity ID fields in conditions
|
||||
for key in ("entity_id", "device_id", "zone"):
|
||||
if key in config:
|
||||
entities.update(await extract_entities_from_value(hass, config[key]))
|
||||
|
||||
# Extract from nested configs
|
||||
for value in config.values():
|
||||
if isinstance(value, (dict, list)):
|
||||
entities.update(await extract_entities_from_condition_config(hass, value))
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def extract_entities_from_action_config(
|
||||
hass: HomeAssistant, config: dict[str, Any] | list
|
||||
) -> set[str]:
|
||||
"""Extract entity IDs from action configuration."""
|
||||
entities = set()
|
||||
|
||||
if not config:
|
||||
return entities
|
||||
|
||||
if isinstance(config, list):
|
||||
for item in config:
|
||||
entities.update(await extract_entities_from_action_config(hass, item))
|
||||
return entities
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return entities
|
||||
|
||||
# Extract entity IDs from direct fields
|
||||
entities.update(await _extract_entities_from_action_fields(hass, config))
|
||||
|
||||
# Extract entities from target configuration
|
||||
entities.update(await _extract_entities_from_target(hass, config))
|
||||
|
||||
# Extract entities from service data
|
||||
entities.update(await _extract_entities_from_service_data(hass, config))
|
||||
|
||||
# Extract from nested configs (like if/then/else, repeat, etc.)
|
||||
entities.update(await _extract_entities_from_nested_configs(hass, config))
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def _extract_entities_from_action_fields(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> set[str]:
|
||||
"""Extract entities from direct action fields."""
|
||||
entities = set()
|
||||
for key in ("entity_id", "device_id"):
|
||||
if key in config:
|
||||
entities.update(await extract_entities_from_value(hass, config[key]))
|
||||
return entities
|
||||
|
||||
|
||||
async def _extract_entities_from_target(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> set[str]:
|
||||
"""Extract entities from target configuration."""
|
||||
entities = set()
|
||||
if "target" in config and isinstance(config["target"], dict):
|
||||
target = config["target"]
|
||||
for key in ("entity_id", "device_id", "area_id", "label_id"):
|
||||
if key in target:
|
||||
entities.update(await extract_entities_from_value(hass, target[key]))
|
||||
return entities
|
||||
|
||||
|
||||
def _get_action_service(config: dict[str, Any]) -> str | None:
|
||||
"""Return the service/action name configured for an action."""
|
||||
service = config.get("service", config.get("action"))
|
||||
return service if isinstance(service, str) else None
|
||||
|
||||
|
||||
def _should_skip_service_data_value(
|
||||
service: str | None,
|
||||
key: str,
|
||||
) -> bool:
|
||||
"""Return if a service data value should not be scanned for entity IDs."""
|
||||
return service is not None and service.startswith("notify.") and key == "target"
|
||||
|
||||
|
||||
async def _extract_entities_from_service_data(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> set[str]:
|
||||
"""Extract entities from service data."""
|
||||
entities = set()
|
||||
if "data" in config:
|
||||
data_value = config["data"]
|
||||
if isinstance(data_value, str):
|
||||
# data field is a template string itself
|
||||
entities.update(await extract_entities_from_value(hass, data_value))
|
||||
elif isinstance(data_value, dict):
|
||||
service = _get_action_service(config)
|
||||
# data field is a dictionary, process all its values
|
||||
for key, value in data_value.items():
|
||||
if _should_skip_service_data_value(service, key):
|
||||
continue
|
||||
entities.update(await extract_entities_from_value(hass, value))
|
||||
return entities
|
||||
|
||||
|
||||
async def _extract_entities_from_nested_configs(
|
||||
hass: HomeAssistant, config: dict[str, Any]
|
||||
) -> set[str]:
|
||||
"""Extract entities from nested configurations."""
|
||||
entities = set()
|
||||
for value in config.values():
|
||||
if isinstance(value, (dict, list)):
|
||||
entities.update(await extract_entities_from_action_config(hass, value))
|
||||
return entities
|
||||
|
||||
|
||||
async def extract_entities_from_value(hass: HomeAssistant, value: Any) -> set[str]:
|
||||
"""Extract entity IDs from a configuration value."""
|
||||
entities = set()
|
||||
|
||||
if isinstance(value, str):
|
||||
# Check if it's a template string using util.is_template_string
|
||||
if is_template_string(value):
|
||||
# Process as template to extract entity references
|
||||
try:
|
||||
template_entities = await async_extract_entities_from_template_string(
|
||||
hass, value
|
||||
)
|
||||
entities.update(template_entities)
|
||||
# pylint: disable-next=broad-exception-caught
|
||||
except Exception as exc: # noqa: BLE001 - Keep broad for unexpected template issues
|
||||
LOGGER.debug(
|
||||
"Failed to extract entities from template: %s, error: %s",
|
||||
value,
|
||||
exc,
|
||||
)
|
||||
elif re.match(rf"^{ENTITY_ID_PATTERN}$", value):
|
||||
# Check if it matches the entity ID pattern with known domains
|
||||
entities.add(value)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
entities.update(await extract_entities_from_value(hass, item))
|
||||
elif (
|
||||
isinstance(value, dict)
|
||||
and "entity" in value
|
||||
and isinstance(value["entity"], str)
|
||||
):
|
||||
# Handle entity dict format like {"entity": "light.living_room"}
|
||||
entities.add(value["entity"])
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced entity in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_entity_references"
|
||||
inspect_events = {
|
||||
EVENT_COMPONENT_LOADED,
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "entities"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_entity_ids: set[str]
|
||||
|
||||
async def async_activate(self) -> None:
|
||||
"""Activate the repair."""
|
||||
await super().async_activate()
|
||||
|
||||
@callback
|
||||
def _state_entity_changed(event_data: Mapping[str, Any]) -> bool:
|
||||
"""Return if a state entity was added or removed."""
|
||||
return (
|
||||
event_data.get("old_state") is None
|
||||
or event_data.get("new_state") is None
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_call_inspect_debouncer(_: Event) -> None:
|
||||
"""Trigger an inspection when a state entity is added or removed."""
|
||||
self.inspect_debouncer.async_schedule_call()
|
||||
|
||||
self._event_subs.add(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_STATE_CHANGED,
|
||||
_async_call_inspect_debouncer,
|
||||
event_filter=_state_entity_changed,
|
||||
),
|
||||
)
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known entity IDs (including ALL/NONE) for this inspection cycle."""
|
||||
self._known_entity_ids = async_get_all_entity_ids(
|
||||
self.hass, include_all_none=True
|
||||
)
|
||||
|
||||
def _should_inspect_entity(self, entity: Any) -> bool:
|
||||
"""Skip disabled automations."""
|
||||
return entity.enabled
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown entity IDs referenced by ``entity`` (incl. templates)."""
|
||||
all_entities = set(entity.referenced_entities)
|
||||
|
||||
# Also extract entities directly from raw configuration if available
|
||||
if hasattr(entity, "raw_config") and entity.raw_config:
|
||||
all_entities.update(
|
||||
await extract_entities_from_automation_config(
|
||||
self.hass, entity.raw_config
|
||||
)
|
||||
)
|
||||
for key in ("trigger", "triggers"):
|
||||
all_entities.difference_update(
|
||||
extract_event_types_from_trigger_config(entity.raw_config.get(key))
|
||||
)
|
||||
|
||||
# Extract entities from Template objects within the automation entity
|
||||
all_entities.update(
|
||||
await extract_template_entities_from_automation_entity(self.hass, entity)
|
||||
)
|
||||
|
||||
return await async_filter_known_entity_ids_with_templates(
|
||||
self.hass,
|
||||
entity_ids=all_entities,
|
||||
known_entity_ids=self._known_entity_ids,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.helpers import floor_registry as fr
|
||||
|
||||
from ....entity_filtering import async_filter_known_floor_ids, async_get_all_floor_ids
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced floors in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_floor_references"
|
||||
inspect_events = {
|
||||
fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "floors"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_floor_ids: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known floor IDs for this inspection cycle."""
|
||||
self._known_floor_ids = async_get_all_floor_ids(self.hass)
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown floor IDs referenced by ``entity``."""
|
||||
return async_filter_known_floor_ids(
|
||||
self.hass,
|
||||
floor_ids=entity.referenced_floors,
|
||||
known_floor_ids=self._known_floor_ids,
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.helpers import label_registry as lr
|
||||
|
||||
from ....entity_filtering import async_filter_known_label_ids, async_get_all_label_ids
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced labels in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_label_references"
|
||||
inspect_events = {
|
||||
lr.EVENT_LABEL_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "labels"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_label_ids: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known label IDs for this inspection cycle."""
|
||||
self._known_label_ids = async_get_all_label_ids(self.hass)
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown label IDs referenced by ``entity``."""
|
||||
return async_filter_known_label_ids(
|
||||
self.hass,
|
||||
label_ids=entity.referenced_labels,
|
||||
known_label_ids=self._known_label_ids,
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components import automation
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
EVENT_SERVICE_REMOVED,
|
||||
)
|
||||
|
||||
from ....entity_filtering import (
|
||||
async_filter_known_services,
|
||||
async_find_services_in_sequence,
|
||||
async_get_all_services,
|
||||
)
|
||||
from ....repairs import AbstractSpookEntityComponentUnknownReferencesRepair
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookEntityComponentUnknownReferencesRepair):
|
||||
"""Spook repair tries to find unknown referenced services in automations."""
|
||||
|
||||
domain = automation.DOMAIN
|
||||
repair = "automation_unknown_service_references"
|
||||
inspect_events = {
|
||||
automation.EVENT_AUTOMATION_RELOADED,
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
EVENT_SERVICE_REMOVED,
|
||||
}
|
||||
inspect_config_entry_changed = True
|
||||
inspect_on_reload = True
|
||||
|
||||
unavailable_entity_class = automation.UnavailableAutomationEntity
|
||||
entity_label = "automation"
|
||||
reference_label = "services"
|
||||
edit_url_pattern = "/config/automation/edit/{unique_id}"
|
||||
|
||||
_known_services: set[str]
|
||||
|
||||
async def _async_setup_inspection(self) -> None:
|
||||
"""Cache known services for this inspection cycle."""
|
||||
self._known_services = async_get_all_services(self.hass)
|
||||
|
||||
def _should_inspect_entity(self, entity: Any) -> bool:
|
||||
"""Skip disabled automations."""
|
||||
return entity.enabled
|
||||
|
||||
async def _async_compute_unknown_references(self, entity: Any) -> set[str]:
|
||||
"""Return unknown services called by ``entity``."""
|
||||
return async_filter_known_services(
|
||||
self.hass,
|
||||
services=async_find_services_in_sequence(entity.action_script.sequence),
|
||||
known_services=self._known_services,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,66 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.blueprint import DOMAIN
|
||||
from homeassistant.components.blueprint.errors import FileAlreadyExists
|
||||
from homeassistant.components.blueprint.importer import fetch_blueprint_from_url
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.blueprint.models import DomainBlueprints
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Blueprint integration service to import an Blueprint from an URL."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "import"
|
||||
schema = {vol.Required("url"): cv.url}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
imported_blueprint = await fetch_blueprint_from_url(
|
||||
self.hass,
|
||||
call.data["url"],
|
||||
)
|
||||
except (TimeoutError, aiohttp.ClientError) as err:
|
||||
msg = "Error fetching blueprint from URL"
|
||||
raise HomeAssistantError(msg) from err
|
||||
|
||||
if imported_blueprint is None:
|
||||
msg = "This url is not supported"
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
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)
|
||||
|
||||
imported_blueprint.blueprint.update_metadata(source_url=call.data["url"])
|
||||
|
||||
try:
|
||||
await domain_blueprints[
|
||||
imported_blueprint.blueprint.domain
|
||||
].async_add_blueprint(
|
||||
imported_blueprint.blueprint,
|
||||
imported_blueprint.suggested_filename,
|
||||
)
|
||||
except FileAlreadyExists as ex:
|
||||
msg = "File already exists"
|
||||
raise HomeAssistantError(msg) from ex
|
||||
except OSError as err:
|
||||
msg = "Error writing file"
|
||||
raise HomeAssistantError(msg) from err
|
||||
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,41 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.cloud.const import DOMAIN as CLOUD_DOMAIN
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
||||
from ...const import DOMAIN
|
||||
from ...entity import SpookEntity, SpookEntityDescription
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hass_nabucasa import Cloud
|
||||
|
||||
from homeassistant.components.cloud.client import CloudClient
|
||||
|
||||
|
||||
class HomeAssistantCloudSpookEntity(SpookEntity):
|
||||
"""Defines an base Spook entity for Home Assistant Cloud related entities."""
|
||||
|
||||
def __init__(
|
||||
self, cloud: Cloud[CloudClient], description: SpookEntityDescription
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(description=description)
|
||||
self._cloud = cloud
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, CLOUD_DOMAIN)},
|
||||
manufacturer="Nabu Casa Inc.",
|
||||
name="Home Assistant Cloud",
|
||||
configuration_url="https://account.nabucasa.com/",
|
||||
)
|
||||
self._attr_unique_id = f"{CLOUD_DOMAIN}_{description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if cloud services are available."""
|
||||
return (
|
||||
super().available and self._cloud.is_logged_in and self._cloud.is_connected
|
||||
)
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.cloud import DOMAIN as CLOUD_DOMAIN
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
|
||||
from ...entity import SpookEntityDescription
|
||||
from .entity import HomeAssistantCloudSpookEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
|
||||
from homeassistant.components.cloud.client import CloudClient
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
SpookEntityDescription,
|
||||
SwitchEntityDescription,
|
||||
):
|
||||
"""Class describing Spook Home Assistant sensor entities."""
|
||||
|
||||
is_on_fn: Callable[[Cloud[CloudClient]], bool | None]
|
||||
set_fn: Callable[[Cloud[CloudClient], bool], Awaitable[Any]]
|
||||
|
||||
|
||||
SWITCHES: tuple[HomeAssistantCloudSpookSwitchEntityDescription, ...] = (
|
||||
HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
key="alexa",
|
||||
entity_id="switch.cloud_alexa",
|
||||
name="Alexa",
|
||||
icon="mdi:account-voice",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda cloud: cloud.client.prefs.alexa_enabled,
|
||||
set_fn=lambda cloud, enabled: cloud.client.prefs.async_update(
|
||||
alexa_enabled=enabled,
|
||||
),
|
||||
),
|
||||
HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
key="alexa_report_state",
|
||||
translation_key="cloud_alexa_report_state",
|
||||
entity_id="switch.cloud_alexa_report_state",
|
||||
icon="mdi:account-voice",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda cloud: cloud.client.prefs.alexa_report_state,
|
||||
set_fn=lambda cloud, enabled: cloud.client.prefs.async_update(
|
||||
alexa_report_state=enabled,
|
||||
),
|
||||
),
|
||||
HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
key="google",
|
||||
entity_id="switch.cloud_google",
|
||||
name="Google Assistant",
|
||||
icon="mdi:google-assistant",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda cloud: cloud.client.prefs.google_enabled,
|
||||
set_fn=lambda cloud, enabled: cloud.client.prefs.async_update(
|
||||
google_enabled=enabled,
|
||||
),
|
||||
),
|
||||
HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
key="google_report_state",
|
||||
translation_key="cloud_google_report_state",
|
||||
entity_id="switch.cloud_google_report_state",
|
||||
icon="mdi:google-assistant",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda cloud: cloud.client.prefs.google_report_state,
|
||||
set_fn=lambda cloud, enabled: cloud.client.prefs.async_update(
|
||||
google_report_state=enabled,
|
||||
),
|
||||
),
|
||||
HomeAssistantCloudSpookSwitchEntityDescription(
|
||||
key="remote",
|
||||
translation_key="cloud_remote",
|
||||
entity_id="switch.cloud_remote",
|
||||
icon="mdi:remote-desktop",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda cloud: cloud.client.prefs.remote_enabled,
|
||||
set_fn=lambda cloud, enabled: cloud.client.prefs.async_update(
|
||||
remote_enabled=enabled,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Spook Home Assistant Cloud switches."""
|
||||
if CLOUD_DOMAIN in hass.config.components:
|
||||
cloud: Cloud[CloudClient] = hass.data[CLOUD_DOMAIN]
|
||||
async_add_entities(
|
||||
HomeAssistantCloudSpookSwitchEntity(cloud, description)
|
||||
for description in SWITCHES
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantCloudSpookSwitchEntity(HomeAssistantCloudSpookEntity, SwitchEntity):
|
||||
"""Spook switch providig Home Asistant Cloud controls."""
|
||||
|
||||
entity_description: HomeAssistantCloudSpookSwitchEntityDescription
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for switch updates."""
|
||||
|
||||
async def _update_state(_: Any) -> None:
|
||||
"""Update state."""
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
self._cloud.client.prefs.async_listen_updates(_update_state),
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon."""
|
||||
if self.entity_description.icon and self.is_on is False:
|
||||
return self.entity_description.icon
|
||||
return super().icon
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return state of the switch."""
|
||||
return self.entity_description.is_on_fn(self._cloud)
|
||||
|
||||
async def async_turn_on(self, **_kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.entity_description.set_fn(self._cloud, True) # noqa: FBT003
|
||||
|
||||
async def async_turn_off(self, **_kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.entity_description.set_fn(self._cloud, False) # noqa: FBT003
|
||||
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,73 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import group
|
||||
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 ....repairs import AbstractSpookRepair
|
||||
|
||||
|
||||
class SpookRepair(AbstractSpookRepair):
|
||||
"""Spook repair tries to find unknown member entities in groups."""
|
||||
|
||||
domain = group.DOMAIN
|
||||
repair = "group_unknown_members"
|
||||
inspect_events = {
|
||||
EVENT_COMPONENT_LOADED,
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
}
|
||||
inspect_config_entry_changed = group.DOMAIN
|
||||
inspect_on_reload = True
|
||||
|
||||
automatically_clean_up_issues = True
|
||||
|
||||
async def async_inspect(self) -> None:
|
||||
"""Trigger a inspection."""
|
||||
LOGGER.debug("Spook is inspecting: %s", self.repair)
|
||||
|
||||
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 platform in platforms:
|
||||
# We don't want to check the old style group platform
|
||||
for entity in 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
|
||||
):
|
||||
self.async_create_issue(
|
||||
issue_id=entity.entity_id,
|
||||
translation_placeholders={
|
||||
"entities": "\n".join(
|
||||
f"- `{entity_id}`" for entity_id in unknown_entities
|
||||
),
|
||||
"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),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,87 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN,
|
||||
SERVICE_HOMEASSISTANT_RESTART,
|
||||
SERVICE_RELOAD_ALL,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
|
||||
from ...entity import SpookEntityDescription
|
||||
from .entity import HomeAssistantSpookEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeAssistantSpookButtonEntityDescription(
|
||||
SpookEntityDescription,
|
||||
ButtonEntityDescription,
|
||||
):
|
||||
"""Class describing Spook Home Assistant button entities."""
|
||||
|
||||
press_fn: Callable[[HomeAssistant], Awaitable[Any]]
|
||||
|
||||
|
||||
BUTTONS: tuple[HomeAssistantSpookButtonEntityDescription, ...] = (
|
||||
HomeAssistantSpookButtonEntityDescription(
|
||||
key="restart",
|
||||
translation_key="homeassistant_restart",
|
||||
entity_id="button.homeassistant_restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda hass: hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_HOMEASSISTANT_RESTART,
|
||||
blocking=True,
|
||||
),
|
||||
),
|
||||
HomeAssistantSpookButtonEntityDescription(
|
||||
key="reload",
|
||||
translation_key="homeassistant_reload",
|
||||
entity_id="button.homeassistant_reload",
|
||||
icon="mdi:auto-fix",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_fn=lambda hass: hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_RELOAD_ALL,
|
||||
blocking=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
_hass: HomeAssistant,
|
||||
_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Spook sensor."""
|
||||
async_add_entities(
|
||||
HomeAssistantSpookButtonEntity(description) for description in BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantSpookButtonEntity(HomeAssistantSpookEntity, ButtonEntity):
|
||||
"""Spook button providig Home Asistant actions."""
|
||||
|
||||
entity_description: HomeAssistantSpookButtonEntityDescription
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self.entity_description.press_fn(self.hass)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Device helpers for Home Assistant services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
|
||||
@callback
|
||||
def async_disable_device_and_parent_if_needed(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Disable a device and its parent when no enabled child devices remain."""
|
||||
device = device_registry.async_get(device_id)
|
||||
if device is None:
|
||||
return
|
||||
|
||||
if device.disabled_by is None:
|
||||
device_registry.async_update_device(
|
||||
device_id=device_id,
|
||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||
)
|
||||
|
||||
if device.via_device_id is None:
|
||||
return
|
||||
|
||||
if all(
|
||||
child.id == device_id or child.disabled_by is not None
|
||||
for child in device_registry.devices.values()
|
||||
if child.via_device_id == device.via_device_id
|
||||
):
|
||||
async_disable_device_and_parent_if_needed(
|
||||
device_registry,
|
||||
device.via_device_id,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_enable_device_and_parent(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Enable a device and its parent device chain."""
|
||||
device = device_registry.async_get(device_id)
|
||||
if device is None:
|
||||
return
|
||||
|
||||
if device.via_device_id is not None:
|
||||
async_enable_device_and_parent(device_registry, device.via_device_id)
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id=device_id,
|
||||
disabled_by=None,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components import homeassistant
|
||||
from homeassistant.const import __version__
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
||||
from ...const import DOMAIN
|
||||
from ...entity import SpookEntity, SpookEntityDescription
|
||||
|
||||
|
||||
class HomeAssistantSpookEntity(SpookEntity):
|
||||
"""Defines an base Spook entity for Home Assistant related entities."""
|
||||
|
||||
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}"
|
||||
@@ -0,0 +1,644 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components import (
|
||||
automation,
|
||||
input_boolean,
|
||||
input_button,
|
||||
input_datetime,
|
||||
input_number,
|
||||
input_select,
|
||||
input_text,
|
||||
persistent_notification,
|
||||
person,
|
||||
script,
|
||||
sun,
|
||||
zone,
|
||||
)
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from ...entity import SpookEntityDescription
|
||||
from ...listeners import async_listen_once_tracked
|
||||
from .entity import HomeAssistantSpookEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime # Moved datetime here
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util.event_type import EventType
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeAssistantSpookSensorEntityDescription(
|
||||
SpookEntityDescription,
|
||||
SensorEntityDescription,
|
||||
):
|
||||
"""Class describing Spook Home Assistant sensor entities."""
|
||||
|
||||
value_fn: Callable[[HomeAssistant], int | None]
|
||||
update_events: set[EventType[Any] | str] = field(default_factory=set)
|
||||
|
||||
|
||||
@callback
|
||||
def _count_active_domain_entities(hass: HomeAssistant, domain: str) -> int:
|
||||
"""Count domain entities that are not restored placeholders."""
|
||||
return sum(
|
||||
(state := hass.states.get(entity_id)) is not None
|
||||
and not state.attributes.get("restored", False)
|
||||
for entity_id in hass.states.async_entity_ids(domain)
|
||||
)
|
||||
|
||||
|
||||
SENSORS: tuple[HomeAssistantSpookSensorEntityDescription, ...] = (
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.AIR_QUALITY,
|
||||
translation_key="homeassistant_air_quality",
|
||||
entity_id="sensor.air_quality",
|
||||
icon="mdi:air-filter",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.AIR_QUALITY)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.ALARM_CONTROL_PANEL,
|
||||
translation_key="homeassistant_alarm_control_panel",
|
||||
entity_id="sensor.alarm_control_panels",
|
||||
icon="mdi:alarm-panel",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(
|
||||
hass.states.async_entity_ids(Platform.ALARM_CONTROL_PANEL),
|
||||
),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="area",
|
||||
translation_key="homeassistant_area",
|
||||
entity_id="sensor.areas",
|
||||
icon="mdi:texture-box",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, ar.EVENT_AREA_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(list(ar.async_get(hass).async_list_areas())),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=automation.DOMAIN,
|
||||
translation_key="homeassistant_automation",
|
||||
entity_id="sensor.automations",
|
||||
icon="mdi:robot",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={automation.EVENT_AUTOMATION_RELOADED},
|
||||
value_fn=lambda hass: _count_active_domain_entities(hass, automation.DOMAIN),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.BINARY_SENSOR,
|
||||
translation_key="homeassistant_binary_sensor",
|
||||
entity_id="sensor.binary_sensors",
|
||||
icon="mdi:numeric-10",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.BINARY_SENSOR)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.BUTTON,
|
||||
translation_key="homeassistant_button",
|
||||
entity_id="sensor.buttons",
|
||||
icon="mdi:gesture-tap",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.BUTTON)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.CALENDAR,
|
||||
translation_key="homeassistant_calendar",
|
||||
entity_id="sensor.calendars",
|
||||
icon="mdi:calendar",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.CALENDAR)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.CAMERA,
|
||||
translation_key="homeassistant_camera",
|
||||
entity_id="sensor.cameras",
|
||||
icon="mdi:cctv",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.CAMERA)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.CLIMATE,
|
||||
translation_key="homeassistant_climate",
|
||||
entity_id="sensor.climate",
|
||||
icon="mdi:thermostat",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.CLIMATE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.COVER,
|
||||
translation_key="homeassistant_cover",
|
||||
entity_id="sensor.covers",
|
||||
icon="mdi:blinds",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.COVER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.DATE,
|
||||
translation_key="homeassistant_date",
|
||||
entity_id="sensor.dates",
|
||||
icon="mdi:calendar-month-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.DATE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.DATETIME,
|
||||
translation_key="homeassistant_datetime",
|
||||
entity_id="sensor.datetimes",
|
||||
icon="mdi:calendar-clock",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.DATETIME)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="device",
|
||||
translation_key="homeassistant_device",
|
||||
entity_id="sensor.devices",
|
||||
icon="mdi:cellphone",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, dr.EVENT_DEVICE_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(dr.async_get(hass).devices),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.DEVICE_TRACKER,
|
||||
translation_key="homeassistant_device_tracker",
|
||||
entity_id="sensor.device_trackers",
|
||||
icon="mdi:cellphone-marker",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(
|
||||
hass.states.async_entity_ids(Platform.DEVICE_TRACKER),
|
||||
),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="entities",
|
||||
translation_key="homeassistant_entities",
|
||||
entity_id="sensor.entities",
|
||||
icon="mdi:counter",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids()),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.FAN,
|
||||
translation_key="homeassistant_fan",
|
||||
entity_id="sensor.fans",
|
||||
icon="mdi:fan",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.FAN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.HUMIDIFIER,
|
||||
translation_key="homeassistant_humidifier",
|
||||
entity_id="sensor.humidifiers",
|
||||
icon="mdi:air-humidifier",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.HUMIDIFIER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="integration",
|
||||
translation_key="homeassistant_integration",
|
||||
entity_id="sensor.integrations",
|
||||
icon="mdi:package-variant-closed",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED},
|
||||
value_fn=lambda hass: len(hass.data["entity_platform"]),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="custom_component",
|
||||
translation_key="homeassistant_custom_component",
|
||||
entity_id="sensor.custom_integrations",
|
||||
icon="mdi:package-variant-closed",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED},
|
||||
value_fn=lambda hass: len(hass.data["custom_components"]),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_boolean.DOMAIN,
|
||||
translation_key="homeassistant_input_boolean",
|
||||
entity_id="sensor.input_booleans",
|
||||
icon="mdi:toggle-switch-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_boolean.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_button.DOMAIN,
|
||||
translation_key="homeassistant_input_button",
|
||||
entity_id="sensor.input_buttons",
|
||||
icon="mdi:gesture-tap-button",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_button.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_datetime.DOMAIN,
|
||||
translation_key="homeassistant_input_datetime",
|
||||
entity_id="sensor.input_datetimes",
|
||||
icon="mdi:clock",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_datetime.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_number.DOMAIN,
|
||||
translation_key="homeassistant_input_number",
|
||||
entity_id="sensor.input_numbers",
|
||||
icon="mdi:ray-vertex",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_number.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_select.DOMAIN,
|
||||
translation_key="homeassistant_input_select",
|
||||
entity_id="sensor.input_selects",
|
||||
icon="mdi:form-dropdown",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_select.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=input_text.DOMAIN,
|
||||
translation_key="homeassistant_input_text",
|
||||
entity_id="sensor.input_texts",
|
||||
icon="mdi:form-textbox",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(input_text.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.IMAGE,
|
||||
translation_key="homeassistant_image",
|
||||
entity_id="sensor.images",
|
||||
icon="mdi:image",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.IMAGE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.LIGHT,
|
||||
translation_key="homeassistant_light",
|
||||
entity_id="sensor.lights",
|
||||
icon="mdi:lightbulb",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.LIGHT)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.LOCK,
|
||||
translation_key="homeassistant_lock",
|
||||
entity_id="sensor.locks",
|
||||
icon="mdi:lock",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.LOCK)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.MEDIA_PLAYER,
|
||||
translation_key="homeassistant_media_player",
|
||||
entity_id="sensor.media_players",
|
||||
icon="mdi:record-player",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.MEDIA_PLAYER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.NUMBER,
|
||||
translation_key="homeassistant_number",
|
||||
entity_id="sensor.numbers",
|
||||
icon="mdi:ray-vertex",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.NUMBER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key="persistent_notification",
|
||||
translation_key="homeassistant_persistent_notification",
|
||||
entity_id="sensor.persistent_notifications",
|
||||
icon="mdi:bell-ring-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={
|
||||
"persistent_notifications_updated",
|
||||
},
|
||||
value_fn=lambda hass: len(
|
||||
# pylint: disable-next=protected-access
|
||||
persistent_notification._async_get_or_create_notifications( # noqa: SLF001
|
||||
hass,
|
||||
),
|
||||
),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=person.DOMAIN,
|
||||
translation_key="homeassistant_person",
|
||||
entity_id="sensor.persons",
|
||||
icon="mdi:account-group",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(person.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.REMOTE,
|
||||
translation_key="homeassistant_remote",
|
||||
entity_id="sensor.remotes",
|
||||
icon="mdi:remote",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.REMOTE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.SCENE,
|
||||
translation_key="homeassistant_scene",
|
||||
entity_id="sensor.scenes",
|
||||
icon="mdi:palette",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.SCENE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=script.DOMAIN,
|
||||
translation_key="homeassistant_script",
|
||||
entity_id="sensor.scripts",
|
||||
icon="mdi:script-text",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: _count_active_domain_entities(hass, script.DOMAIN),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.SELECT,
|
||||
translation_key="homeassistant_select",
|
||||
entity_id="sensor.selects",
|
||||
icon="mdi:format-list-bulleted",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.SELECT)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.SENSOR,
|
||||
translation_key="homeassistant_sensor",
|
||||
entity_id="sensor.sensors",
|
||||
icon="mdi:eye",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.SENSOR)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.SIREN,
|
||||
translation_key="homeassistant_siren",
|
||||
entity_id="sensor.sirens",
|
||||
icon="mdi:bullhorn",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.SIREN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=sun.DOMAIN,
|
||||
translation_key="homeassistant_sun",
|
||||
entity_id="sensor.suns",
|
||||
icon="mdi:emoticon-cool",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(sun.DOMAIN)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.STT,
|
||||
translation_key="homeassistant_stt",
|
||||
entity_id="sensor.stt",
|
||||
icon="mdi:microphone-message",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.STT)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.SWITCH,
|
||||
translation_key="homeassistant_switch",
|
||||
entity_id="sensor.switches",
|
||||
icon="mdi:toggle-switch",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.SWITCH)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.TEXT,
|
||||
translation_key="homeassistant_text",
|
||||
entity_id="sensor.texts",
|
||||
icon="mdi:form-textbox",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.TEXT)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.TIME,
|
||||
translation_key="homeassistant_time",
|
||||
entity_id="sensor.times",
|
||||
icon="mdi:clock-time-eight-outline",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.TIME)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.TODO,
|
||||
translation_key="homeassistant_todo",
|
||||
entity_id="sensor.todos",
|
||||
icon="mdi:clipboard-list",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.TODO)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.TTS,
|
||||
translation_key="homeassistant_tts",
|
||||
entity_id="sensor.tts",
|
||||
icon="mdi:speaker-message",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.TTS)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.VACUUM,
|
||||
translation_key="homeassistant_vacuum",
|
||||
entity_id="sensor.vacuums",
|
||||
icon="mdi:vacuum",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.VACUUM)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.UPDATE,
|
||||
translation_key="homeassistant_update",
|
||||
entity_id="sensor.update",
|
||||
icon="mdi:cellphone-arrow-down",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.UPDATE)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.WATER_HEATER,
|
||||
translation_key="homeassistant_water_heater",
|
||||
entity_id="sensor.water_heaters",
|
||||
icon="mdi:water-boiler",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.WATER_HEATER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=Platform.WEATHER,
|
||||
translation_key="homeassistant_weather",
|
||||
entity_id="sensor.weather",
|
||||
icon="mdi:weather-cloudy",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(Platform.WEATHER)),
|
||||
),
|
||||
HomeAssistantSpookSensorEntityDescription(
|
||||
key=zone.DOMAIN,
|
||||
translation_key="homeassistant_zone",
|
||||
entity_id="sensor.zones",
|
||||
icon="mdi:selection-marker",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
update_events={EVENT_COMPONENT_LOADED, er.EVENT_ENTITY_REGISTRY_UPDATED},
|
||||
value_fn=lambda hass: len(hass.states.async_entity_ids(zone.DOMAIN)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
_hass: HomeAssistant,
|
||||
_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Spook sensor."""
|
||||
async_add_entities(
|
||||
HomeAssistantSpookSensorEntity(description) for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class HomeAssistantSpookSensorEntity(HomeAssistantSpookEntity, SensorEntity):
|
||||
"""Spook sensor providig Home Asistant information."""
|
||||
|
||||
entity_description: HomeAssistantSpookSensorEntityDescription
|
||||
_unsub_debouncer: Callable[[], None] | None = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register for sensor updates."""
|
||||
|
||||
@callback
|
||||
def _debounced_update(
|
||||
_now: datetime | None = None,
|
||||
) -> None:
|
||||
"""Update state after debounce."""
|
||||
self._unsub_debouncer = None
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_state(_: Event) -> None:
|
||||
"""Update state."""
|
||||
if self._unsub_debouncer:
|
||||
self._unsub_debouncer()
|
||||
self._unsub_debouncer = async_call_later(self.hass, 5, _debounced_update)
|
||||
|
||||
for event in self.entity_description.update_events:
|
||||
self.async_on_remove(self.hass.bus.async_listen(event, _update_state))
|
||||
|
||||
self.async_on_remove(
|
||||
async_listen_once_tracked(
|
||||
self.hass, EVENT_HOMEASSISTANT_STARTED, _update_state
|
||||
),
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Clean up debounce timer."""
|
||||
if self._unsub_debouncer:
|
||||
self._unsub_debouncer()
|
||||
self._unsub_debouncer = None
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the sensor value."""
|
||||
return self.entity_description.value_fn(self.hass)
|
||||
@@ -0,0 +1 @@
|
||||
"""Spook - Your homie."""
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,40 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add an alias to an area."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_alias_to_area"
|
||||
schema = {
|
||||
vol.Required("area_id"): cv.string,
|
||||
vol.Required("alias"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""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)
|
||||
|
||||
aliases = area.aliases.copy()
|
||||
area_registry.async_update(
|
||||
call.data["area_id"],
|
||||
aliases=aliases.union(call.data["alias"]),
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add an alias to a floor."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_alias_to_floor"
|
||||
schema = {
|
||||
vol.Required("floor_id"): cv.string,
|
||||
vol.Required("alias"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""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)
|
||||
|
||||
aliases = floor.aliases.copy()
|
||||
floor_registry.async_update(
|
||||
call.data["floor_id"],
|
||||
aliases=aliases.union(call.data["alias"]),
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add an area to a floor."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_area_to_floor"
|
||||
schema = {
|
||||
vol.Required("floor_id"): cv.string,
|
||||
vol.Required("area_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""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)
|
||||
|
||||
area_registry = ar.async_get(self.hass)
|
||||
for area_id in call.data["area_id"]:
|
||||
area_registry.async_update(
|
||||
area_id=area_id,
|
||||
floor_id=call.data["floor_id"],
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
device_registry as dr,
|
||||
)
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add a device to an area."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_device_to_area"
|
||||
schema = {
|
||||
vol.Required("area_id"): cv.string,
|
||||
vol.Required("device_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""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)
|
||||
|
||||
device_registry = dr.async_get(self.hass)
|
||||
for device_id in call.data["device_id"]:
|
||||
device_registry.async_update_device(
|
||||
device_id,
|
||||
area_id=call.data["area_id"],
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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 ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add a entity to an area."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_entity_to_area"
|
||||
schema = {
|
||||
vol.Required("area_id"): cv.string,
|
||||
vol.Required("entity_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""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)
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
for entity_id in call.data["entity_id"]:
|
||||
entity_registry.async_update_entity(
|
||||
entity_id,
|
||||
area_id=call.data["area_id"],
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
label_registry as lr,
|
||||
)
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add a label to an area."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_label_to_area"
|
||||
schema = {
|
||||
vol.Required("label_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required("area_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
label_registry = lr.async_get(self.hass)
|
||||
for label_id in call.data["label_id"]:
|
||||
if not label_registry.async_get_label(label_id):
|
||||
msg = f"Label {label_id} not found"
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
area_registry = ar.async_get(self.hass)
|
||||
for area_id in call.data["area_id"]:
|
||||
if area_entry := area_registry.async_get_area(area_id):
|
||||
labels = area_entry.labels.copy()
|
||||
labels.update(call.data["label_id"])
|
||||
area_registry.async_update(area_id, labels=labels)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
label_registry as lr,
|
||||
)
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add a label to a device."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_label_to_device"
|
||||
schema = {
|
||||
vol.Required("label_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required("device_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
label_registry = lr.async_get(self.hass)
|
||||
for label_id in call.data["label_id"]:
|
||||
if not label_registry.async_get_label(label_id):
|
||||
msg = f"Label {label_id} not found"
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
device_registry = dr.async_get(self.hass)
|
||||
for device_id in call.data["device_id"]:
|
||||
if device_entry := device_registry.async_get(device_id):
|
||||
labels = device_entry.labels.copy()
|
||||
labels.update(call.data["label_id"])
|
||||
device_registry.async_update_device(device_id, labels=labels)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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,
|
||||
label_registry as lr,
|
||||
)
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant service to add a label to an entity."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "add_label_to_entity"
|
||||
schema = {
|
||||
vol.Required("label_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required("entity_id"): vol.All(cv.ensure_list, [cv.string]),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
label_registry = lr.async_get(self.hass)
|
||||
for label_id in call.data["label_id"]:
|
||||
if not label_registry.async_get_label(label_id):
|
||||
msg = f"Label {label_id} not found"
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
for entity_id in call.data["entity_id"]:
|
||||
if entity_entry := entity_registry.async_get(entity_id):
|
||||
labels = entity_entry.labels.copy()
|
||||
labels.update(call.data["label_id"])
|
||||
entity_registry.async_update_entity(entity_id, labels=labels)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.homeassistant import DOMAIN
|
||||
from homeassistant.helpers import area_registry as ar, config_validation as cv
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant area service to create areas on the fly."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "create_area"
|
||||
schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Optional("aliases"): [cv.string],
|
||||
vol.Optional("icon"): cv.icon,
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
area_registry = ar.async_get(self.hass)
|
||||
area_registry.async_create(
|
||||
name=call.data["name"],
|
||||
aliases=call.data.get("aliases"),
|
||||
icon=call.data.get("icon"),
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Spook - Your homie."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.homeassistant import DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv, floor_registry as fr
|
||||
|
||||
from ....services import AbstractSpookAdminService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import ServiceCall
|
||||
|
||||
|
||||
class SpookService(AbstractSpookAdminService):
|
||||
"""Home Assistant floor service to create floors on the fly."""
|
||||
|
||||
domain = DOMAIN
|
||||
service = "create_floor"
|
||||
schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Optional("aliases"): [cv.string],
|
||||
vol.Optional("icon"): cv.icon,
|
||||
vol.Optional("level"): vol.Coerce(int),
|
||||
}
|
||||
|
||||
async def async_handle_service(self, call: ServiceCall) -> None:
|
||||
"""Handle the service call."""
|
||||
floor_registry = fr.async_get(self.hass)
|
||||
floor_registry.async_create(
|
||||
name=call.data["name"],
|
||||
aliases=call.data.get("aliases"),
|
||||
icon=call.data.get("icon"),
|
||||
level=call.data.get("level"),
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user