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
4 changes: 2 additions & 2 deletions homeassistant/components/dwd_weather_warnings/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'latitude' and 'longitude'.",
"description": "To identify the desired region, either the warncell ID / name or device tracker is required. The provided device tracker has to contain the attributes 'Latitude' and 'Longitude'.",
"data": {
"region_identifier": "Warncell ID or name",
"region_device_tracker": "Device tracker entity"
Expand All @@ -14,7 +14,7 @@
"ambiguous_identifier": "The region identifier and device tracker can not be specified together.",
"invalid_identifier": "The specified region identifier / device tracker is invalid.",
"entity_not_found": "The specified device tracker entity was not found.",
"attribute_not_found": "The required `latitude` or `longitude` attribute was not found in the specified device tracker."
"attribute_not_found": "The required attributes 'Latitude' and 'Longitude' were not found in the specified device tracker."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
Expand Down
130 changes: 128 additions & 2 deletions homeassistant/components/history_stats/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
from __future__ import annotations

from collections.abc import Mapping
from datetime import timedelta
from typing import Any, cast

import voluptuous as vol

from homeassistant.components import websocket_api
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_STATE, CONF_TYPE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.schema_config_entry_flow import (
SchemaCommonFlowHandler,
SchemaConfigFlowHandler,
Expand All @@ -26,6 +30,7 @@
TextSelector,
TextSelectorConfig,
)
from homeassistant.helpers.template import Template

from .const import (
CONF_DURATION,
Expand All @@ -37,14 +42,21 @@
DEFAULT_NAME,
DOMAIN,
)
from .coordinator import HistoryStatsUpdateCoordinator
from .data import HistoryStats
from .sensor import HistoryStatsSensor


def _validate_two_period_keys(user_input: dict[str, Any]) -> None:
if sum(param in user_input for param in CONF_PERIOD_KEYS) != 2:
raise SchemaFlowError("only_two_keys_allowed")


async def validate_options(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
"""Validate options selected."""
if sum(param in user_input for param in CONF_PERIOD_KEYS) != 2:
raise SchemaFlowError("only_two_keys_allowed")
_validate_two_period_keys(user_input)

handler.parent_handler._async_abort_entries_match({**handler.options, **user_input}) # noqa: SLF001

Expand Down Expand Up @@ -97,12 +109,14 @@ async def validate_options(
"options": SchemaFlowFormStep(
schema=DATA_SCHEMA_OPTIONS,
validate_user_input=validate_options,
preview="history_stats",
),
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
DATA_SCHEMA_OPTIONS,
validate_user_input=validate_options,
preview="history_stats",
),
}

Expand All @@ -116,3 +130,115 @@ class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
"""Return config entry title."""
return cast(str, options[CONF_NAME])

@staticmethod
async def async_setup_preview(hass: HomeAssistant) -> None:
"""Set up preview WS API."""
websocket_api.async_register_command(hass, ws_start_preview)


@websocket_api.websocket_command(
{
vol.Required("type"): "history_stats/start_preview",
vol.Required("flow_id"): str,
vol.Required("flow_type"): vol.Any("config_flow", "options_flow"),
vol.Required("user_input"): dict,
}
)
@websocket_api.async_response
async def ws_start_preview(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Generate a preview."""
if msg["flow_type"] == "config_flow":
flow_status = hass.config_entries.flow.async_get(msg["flow_id"])
flow_sets = hass.config_entries.flow._handler_progress_index.get( # noqa: SLF001
flow_status["handler"]
)
options = {}
assert flow_sets
for active_flow in flow_sets:
options = active_flow._common_handler.options # type: ignore [attr-defined] # noqa: SLF001
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
entity_id = options[CONF_ENTITY_ID]
name = options[CONF_NAME]
else:
flow_status = hass.config_entries.options.async_get(msg["flow_id"])
config_entry = hass.config_entries.async_get_entry(flow_status["handler"])
if not config_entry:
raise HomeAssistantError("Config entry not found")
entity_id = config_entry.options[CONF_ENTITY_ID]
name = config_entry.options[CONF_NAME]

@callback
def async_preview_updated(
last_exception: Exception | None, state: str, attributes: Mapping[str, Any]
) -> None:
"""Forward config entry state events to websocket."""
if last_exception:
connection.send_message(
websocket_api.event_message(
msg["id"], {"error": str(last_exception) or "Unknown error"}
)
)
else:
connection.send_message(
websocket_api.event_message(
msg["id"], {"attributes": attributes, "state": state}
)
)

for param in CONF_PERIOD_KEYS:
if param in msg["user_input"] and not bool(msg["user_input"][param]):
del msg["user_input"][param] # Remove falsy values before counting keys

validated_data: Any = None
try:
validated_data = DATA_SCHEMA_OPTIONS(msg["user_input"])
except vol.Invalid as ex:
connection.send_error(msg["id"], "invalid_schema", str(ex))
return

try:
_validate_two_period_keys(validated_data)
except SchemaFlowError:
connection.send_error(
msg["id"],
"invalid_schema",
f"Exactly two of {', '.join(CONF_PERIOD_KEYS)} required",
)
return

sensor_type = validated_data.get(CONF_TYPE)
entity_states = validated_data.get(CONF_STATE)
start = validated_data.get(CONF_START)
end = validated_data.get(CONF_END)
duration = validated_data.get(CONF_DURATION)

history_stats = HistoryStats(
hass,
entity_id,
entity_states,
Template(start, hass) if start else None,
Template(end, hass) if end else None,
timedelta(**duration) if duration else None,
True,
)
coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, None, name, True)
await coordinator.async_refresh()
preview_entity = HistoryStatsSensor(
hass, coordinator, sensor_type, name, None, entity_id
)
preview_entity.hass = hass

connection.send_result(msg["id"])
cancel_listener = coordinator.async_setup_state_listener()
cancel_preview = await preview_entity.async_start_preview(async_preview_updated)

def unsub() -> None:
cancel_listener()
cancel_preview()

connection.subscriptions[msg["id"]] = unsub
7 changes: 7 additions & 0 deletions homeassistant/components/history_stats/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ def __init__(
history_stats: HistoryStats,
config_entry: ConfigEntry | None,
name: str,
preview: bool = False,
) -> None:
"""Initialize DataUpdateCoordinator."""
self._history_stats = history_stats
self._subscriber_count = 0
self._at_start_listener: CALLBACK_TYPE | None = None
self._track_events_listener: CALLBACK_TYPE | None = None
self._preview = preview
super().__init__(
hass,
_LOGGER,
Expand Down Expand Up @@ -104,3 +106,8 @@ async def _async_update_data(self) -> HistoryStatsState:
return await self._history_stats.async_update(None)
except (TemplateError, TypeError, ValueError) as ex:
raise UpdateFailed(ex) from ex

async def async_refresh(self) -> None:
"""Refresh data and log errors."""
log_failures = not self._preview
await self._async_refresh(log_failures)
6 changes: 5 additions & 1 deletion homeassistant/components/history_stats/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __init__(
start: Template | None,
end: Template | None,
duration: datetime.timedelta | None,
preview: bool = False,
) -> None:
"""Init the history stats manager."""
self.hass = hass
Expand All @@ -59,6 +60,7 @@ def __init__(
self._duration = duration
self._start = start
self._end = end
self._preview = preview

self._pending_events: list[Event[EventStateChangedData]] = []
self._query_count = 0
Expand All @@ -70,7 +72,9 @@ async def async_update(
# Get previous values of start and end
previous_period_start, previous_period_end = self._period
# Parse templates
self._period = async_calculate_period(self._duration, self._start, self._end)
self._period = async_calculate_period(
self._duration, self._start, self._end, log_errors=not self._preview
)
# Get the current period
current_period_start, current_period_end = self._period

Expand Down
13 changes: 9 additions & 4 deletions homeassistant/components/history_stats/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def async_calculate_period(
duration: datetime.timedelta | None,
start_template: Template | None,
end_template: Template | None,
log_errors: bool = True,
) -> tuple[datetime.datetime, datetime.datetime]:
"""Parse the templates and return the period."""
bounds: dict[str, datetime.datetime | None] = {
Expand All @@ -37,13 +38,17 @@ def async_calculate_period(
if template is None:
continue
try:
rendered = template.async_render()
rendered = template.async_render(
log_fn=None if log_errors else lambda *args, **kwargs: None
)
except (TemplateError, TypeError) as ex:
if ex.args and not ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"
if (
log_errors
and ex.args
and not ex.args[0].startswith("UndefinedError: 'None' has no attribute")
):
_LOGGER.error("Error parsing template for field %s", bound, exc_info=ex)
raise
raise type(ex)(f"Error parsing template for field {bound}: {ex}") from ex
if isinstance(rendered, str):
bounds[bound] = dt_util.parse_datetime(rendered)
if bounds[bound] is not None:
Expand Down
32 changes: 31 additions & 1 deletion homeassistant/components/history_stats/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from abc import abstractmethod
from collections.abc import Callable, Mapping
import datetime
from typing import Any

Expand All @@ -23,7 +24,7 @@
PERCENTAGE,
UnitOfTime,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
Expand Down Expand Up @@ -183,6 +184,9 @@ def __init__(
) -> None:
"""Initialize the HistoryStats sensor."""
super().__init__(coordinator, name)
self._preview_callback: (
Callable[[Exception | None, str, Mapping[str, Any]], None] | None
) = None
self._attr_native_unit_of_measurement = UNITS[sensor_type]
self._type = sensor_type
self._attr_unique_id = unique_id
Expand Down Expand Up @@ -212,3 +216,29 @@ def _process_update(self) -> None:
self._attr_native_value = pretty_ratio(state.seconds_matched, state.period)
elif self._type == CONF_TYPE_COUNT:
self._attr_native_value = state.match_count

if self._preview_callback:
calculated_state = self._async_calculate_state()
self._preview_callback(
None, calculated_state.state, calculated_state.attributes
)

async def async_start_preview(
self,
preview_callback: Callable[[Exception | None, str, Mapping[str, Any]], None],
) -> CALLBACK_TYPE:
"""Render a preview."""

self.async_on_remove(
self.coordinator.async_add_listener(self._process_update, None)
)

self._preview_callback = preview_callback
calculated_state = self._async_calculate_state()
preview_callback(
self.coordinator.last_exception,
calculated_state.state,
calculated_state.attributes,
)

return self._call_on_remove_callbacks
10 changes: 4 additions & 6 deletions homeassistant/components/homee/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr

from .const import DOMAIN
Expand Down Expand Up @@ -53,12 +53,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
try:
await homee.get_access_token()
except HomeeConnectionFailedException as exc:
raise ConfigEntryNotReady(
f"Connection to Homee failed: {exc.__cause__}"
) from exc
raise ConfigEntryNotReady(f"Connection to Homee failed: {exc.reason}") from exc
except HomeeAuthFailedException as exc:
raise ConfigEntryNotReady(
f"Authentication to Homee failed: {exc.__cause__}"
raise ConfigEntryAuthFailed(
f"Authentication to Homee failed: {exc.reason}"
) from exc

hass.loop.create_task(homee.run())
Expand Down
Loading
Loading