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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.loader import (
async_get_custom_components,
async_get_loaded_integration,
)
from homeassistant.util.location import async_detect_location_info

from .alexa_config import entity_supported as entity_supported_by_alexa
Expand Down Expand Up @@ -431,6 +435,79 @@ class DownloadSupportPackageView(HomeAssistantView):
url = "/api/cloud/support_package"
name = "api:cloud:support_package"

async def _get_integration_info(self, hass: HomeAssistant) -> dict[str, Any]:
"""Collect information about active and custom integrations."""
# Get loaded components from hass.config.components
loaded_components = hass.config.components.copy()

# Get custom integrations
custom_domains = set()
with suppress(Exception):
custom_domains = set(await async_get_custom_components(hass))

# Separate built-in and custom integrations
builtin_integrations = []
custom_integrations = []

for domain in sorted(loaded_components):
try:
integration = async_get_loaded_integration(hass, domain)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package
# generation. If we can't get integration info,
# just add the domain
if domain in custom_domains:
custom_integrations.append(
{
"domain": domain,
"name": "Unknown",
"version": "Unknown",
"documentation": "Unknown",
}
)
else:
builtin_integrations.append(
{
"domain": domain,
"name": "Unknown",
}
)
else:
if domain in custom_domains:
# This is a custom integration
# include version and documentation link
version = (
str(integration.version) if integration.version else "Unknown"
)
if not (documentation := integration.documentation):
documentation = "Unknown"

custom_integrations.append(
{
"domain": domain,
"name": integration.name,
"version": version,
"documentation": documentation,
}
)
else:
# This is a built-in integration.
# No version needed, as it is always the same as the
# Home Assistant version
builtin_integrations.append(
{
"domain": domain,
"name": integration.name,
}
)

return {
"builtin_count": len(builtin_integrations),
"builtin_integrations": builtin_integrations,
"custom_count": len(custom_integrations),
"custom_integrations": custom_integrations,
}

async def _generate_markdown(
self,
hass: HomeAssistant,
Expand All @@ -453,6 +530,38 @@ def get_domain_table_markdown(domain_info: dict[str, Any]) -> str:
markdown = "## System Information\n\n"
markdown += get_domain_table_markdown(hass_info)

# Add integration information
try:
integration_info = await self._get_integration_info(hass)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
# If there's any error getting integration info, just note it
markdown += "## Active integrations\n\n"
markdown += "Unable to collect integration information\n\n"
else:
markdown += "## Active Integrations\n\n"
markdown += f"Built-in integrations: {integration_info['builtin_count']}\n"
markdown += f"Custom integrations: {integration_info['custom_count']}\n\n"

# Built-in integrations
if integration_info["builtin_integrations"]:
markdown += "<details><summary>Built-in integrations</summary>\n\n"
markdown += "Domain | Name\n"
markdown += "--- | ---\n"
for integration in integration_info["builtin_integrations"]:
markdown += f"{integration['domain']} | {integration['name']}\n"
markdown += "\n</details>\n\n"

# Custom integrations
if integration_info["custom_integrations"]:
markdown += "<details><summary>Custom integrations</summary>\n\n"
markdown += "Domain | Name | Version | Documentation\n"
markdown += "--- | --- | --- | ---\n"
for integration in integration_info["custom_integrations"]:
doc_url = integration.get("documentation") or "N/A"
markdown += f"{integration['domain']} | {integration['name']} | {integration['version']} | {doc_url}\n"
markdown += "\n</details>\n\n"

for domain, domain_info in domains_info.items():
domain_info_md = get_domain_table_markdown(domain_info)
markdown += (
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/esphome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==41.0.0",
"aioesphomeapi==41.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.3.0"
],
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/history_stats/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Diagnostics support for history_stats."""

from __future__ import annotations

from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er


async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""

registry = er.async_get(hass)
entities = registry.entities.get_entries_for_config_entry_id(config_entry.entry_id)

return {
"config_entry": config_entry.as_dict(),
"entity": [entity.extended_dict for entity in entities],
}
135 changes: 5 additions & 130 deletions homeassistant/helpers/template/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ast import literal_eval
import asyncio
import collections.abc
from collections.abc import Callable, Generator, Iterable, MutableSequence
from collections.abc import Callable, Generator, Iterable
from contextlib import AbstractContextManager
from contextvars import ContextVar
from copy import deepcopy
Expand Down Expand Up @@ -2245,31 +2245,6 @@ def is_number(value):
return True


def _is_list(value: Any) -> bool:
"""Return whether a value is a list."""
return isinstance(value, list)


def _is_set(value: Any) -> bool:
"""Return whether a value is a set."""
return isinstance(value, set)


def _is_tuple(value: Any) -> bool:
"""Return whether a value is a tuple."""
return isinstance(value, tuple)


def _to_set(value: Any) -> set[Any]:
"""Convert value to set."""
return set(value)


def _to_tuple(value):
"""Convert value to tuple."""
return tuple(value)


def _is_datetime(value: Any) -> bool:
"""Return whether a value is a datetime."""
return isinstance(value, datetime)
Expand Down Expand Up @@ -2487,98 +2462,11 @@ def iif(
return if_false


def shuffle(*args: Any, seed: Any = None) -> MutableSequence[Any]:
"""Shuffle a list, either with a seed or without."""
if not args:
raise TypeError("shuffle expected at least 1 argument, got 0")

# If first argument is iterable and more than 1 argument provided
# but not a named seed, then use 2nd argument as seed.
if isinstance(args[0], Iterable):
items = list(args[0])
if len(args) > 1 and seed is None:
seed = args[1]
elif len(args) == 1:
raise TypeError(f"'{type(args[0]).__name__}' object is not iterable")
else:
items = list(args)

if seed:
r = random.Random(seed)
r.shuffle(items)
else:
random.shuffle(items)
return items


def typeof(value: Any) -> Any:
"""Return the type of value passed to debug types."""
return value.__class__.__name__


def flatten(value: Iterable[Any], levels: int | None = None) -> list[Any]:
"""Flattens list of lists."""
if not isinstance(value, Iterable) or isinstance(value, str):
raise TypeError(f"flatten expected a list, got {type(value).__name__}")

flattened: list[Any] = []
for item in value:
if isinstance(item, Iterable) and not isinstance(item, str):
if levels is None:
flattened.extend(flatten(item))
elif levels >= 1:
flattened.extend(flatten(item, levels=(levels - 1)))
else:
flattened.append(item)
else:
flattened.append(item)
return flattened


def intersect(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
"""Return the common elements between two lists."""
if not isinstance(value, Iterable) or isinstance(value, str):
raise TypeError(f"intersect expected a list, got {type(value).__name__}")
if not isinstance(other, Iterable) or isinstance(other, str):
raise TypeError(f"intersect expected a list, got {type(other).__name__}")

return list(set(value) & set(other))


def difference(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
"""Return elements in first list that are not in second list."""
if not isinstance(value, Iterable) or isinstance(value, str):
raise TypeError(f"difference expected a list, got {type(value).__name__}")
if not isinstance(other, Iterable) or isinstance(other, str):
raise TypeError(f"difference expected a list, got {type(other).__name__}")

return list(set(value) - set(other))


def union(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
"""Return all unique elements from both lists combined."""
if not isinstance(value, Iterable) or isinstance(value, str):
raise TypeError(f"union expected a list, got {type(value).__name__}")
if not isinstance(other, Iterable) or isinstance(other, str):
raise TypeError(f"union expected a list, got {type(other).__name__}")

return list(set(value) | set(other))


def symmetric_difference(value: Iterable[Any], other: Iterable[Any]) -> list[Any]:
"""Return elements that are in either list but not in both."""
if not isinstance(value, Iterable) or isinstance(value, str):
raise TypeError(
f"symmetric_difference expected a list, got {type(value).__name__}"
)
if not isinstance(other, Iterable) or isinstance(other, str):
raise TypeError(
f"symmetric_difference expected a list, got {type(other).__name__}"
)

return list(set(value) ^ set(other))


def combine(*args: Any, recursive: bool = False) -> dict[Any, Any]:
"""Combine multiple dictionaries into one."""
if not args:
Expand Down Expand Up @@ -2760,35 +2648,31 @@ def __init__(
self.add_extension("jinja2.ext.loopcontrols")
self.add_extension("jinja2.ext.do")
self.add_extension("homeassistant.helpers.template.extensions.Base64Extension")
self.add_extension(
"homeassistant.helpers.template.extensions.CollectionExtension"
)
self.add_extension("homeassistant.helpers.template.extensions.CryptoExtension")
self.add_extension("homeassistant.helpers.template.extensions.MathExtension")
self.add_extension("homeassistant.helpers.template.extensions.RegexExtension")
self.add_extension("homeassistant.helpers.template.extensions.StringExtension")

self.globals["apply"] = apply
self.globals["as_datetime"] = as_datetime
self.globals["as_function"] = as_function
self.globals["as_local"] = dt_util.as_local
self.globals["as_timedelta"] = as_timedelta
self.globals["as_timestamp"] = forgiving_as_timestamp
self.globals["bool"] = forgiving_boolean
self.globals["combine"] = combine
self.globals["difference"] = difference
self.globals["flatten"] = flatten
self.globals["float"] = forgiving_float
self.globals["iif"] = iif
self.globals["int"] = forgiving_int
self.globals["intersect"] = intersect
self.globals["is_number"] = is_number
self.globals["merge_response"] = merge_response
self.globals["pack"] = struct_pack
self.globals["set"] = _to_set
self.globals["shuffle"] = shuffle
self.globals["strptime"] = strptime
self.globals["symmetric_difference"] = symmetric_difference
self.globals["timedelta"] = timedelta
self.globals["tuple"] = _to_tuple
self.globals["typeof"] = typeof
self.globals["union"] = union
self.globals["unpack"] = struct_unpack
self.globals["version"] = version
self.globals["zip"] = zip
Expand All @@ -2803,40 +2687,31 @@ def __init__(
self.filters["bool"] = forgiving_boolean
self.filters["combine"] = combine
self.filters["contains"] = contains
self.filters["difference"] = difference
self.filters["flatten"] = flatten
self.filters["float"] = forgiving_float_filter
self.filters["from_json"] = from_json
self.filters["from_hex"] = from_hex
self.filters["iif"] = iif
self.filters["int"] = forgiving_int_filter
self.filters["intersect"] = intersect
self.filters["is_defined"] = fail_when_undefined
self.filters["is_number"] = is_number
self.filters["multiply"] = multiply
self.filters["ord"] = ord
self.filters["pack"] = struct_pack
self.filters["random"] = random_every_time
self.filters["round"] = forgiving_round
self.filters["shuffle"] = shuffle
self.filters["symmetric_difference"] = symmetric_difference
self.filters["timestamp_custom"] = timestamp_custom
self.filters["timestamp_local"] = timestamp_local
self.filters["timestamp_utc"] = timestamp_utc
self.filters["to_json"] = to_json
self.filters["typeof"] = typeof
self.filters["union"] = union
self.filters["unpack"] = struct_unpack
self.filters["version"] = version

self.tests["apply"] = apply
self.tests["contains"] = contains
self.tests["datetime"] = _is_datetime
self.tests["is_number"] = is_number
self.tests["list"] = _is_list
self.tests["set"] = _is_set
self.tests["string_like"] = _is_string_like
self.tests["tuple"] = _is_tuple

if hass is None:
return
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/helpers/template/extensions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Home Assistant template extensions."""

from .base64 import Base64Extension
from .collection import CollectionExtension
from .crypto import CryptoExtension
from .math import MathExtension
from .regex import RegexExtension
from .string import StringExtension

__all__ = [
"Base64Extension",
"CollectionExtension",
"CryptoExtension",
"MathExtension",
"RegexExtension",
Expand Down
Loading
Loading