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
3 changes: 3 additions & 0 deletions homeassistant/components/environment_canada/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"get_forecasts": {
"service": "mdi:weather-cloudy-clock"
},
"get_precipitation_forecast": {
"service": "mdi:weather-pouring"
},
"set_radar_type": {
"service": "mdi:radar"
}
Expand Down
67 changes: 65 additions & 2 deletions homeassistant/components/environment_canada/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@

from typing import Any

from env_canada import ECWeather
from env_canada import ECPrecipForecast, ECWeather
import probatio

from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, service
Expand All @@ -17,11 +22,29 @@
{probatio.Required(ATTR_CONFIG_ENTRY_ID): cv.string}
)

SERVICE_GET_PRECIPITATION_FORECAST = "get_precipitation_forecast"
SERVICE_GET_PRECIPITATION_FORECAST_SCHEMA = probatio.Schema(
{
probatio.Required(ATTR_CONFIG_ENTRY_ID): cv.string,
probatio.Optional("precip_type"): probatio.In(["auto", "rain", "snow"]),
probatio.Optional("past_minutes"): probatio.All(int, probatio.Range(0, 180)),
probatio.Optional("future_minutes"): probatio.All(int, probatio.Range(0, 72)),
probatio.Optional("hourly_hours"): probatio.All(int, probatio.Range(0, 48)),
}
)

SNAKE_MAPPING = {
"alertColourLevel": "alert_colour_level",
"expiryTime": "expiry_time",
}

PRECIP_FORECAST_OPTIONS = (
"precip_type",
"past_minutes",
"future_minutes",
"hourly_hours",
)


async def _async_get_alerts(call: ServiceCall) -> dict[str, Any]:
"""Return the active alerts."""
Expand All @@ -46,6 +69,39 @@ async def _async_get_alerts(call: ServiceCall) -> dict[str, Any]:
}


async def _async_get_precipitation_forecast(call: ServiceCall) -> dict[str, Any]:
"""Return the precipitation forecast series."""
entry = service.async_get_config_entry(
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
)

# A fresh object per call, rather than one shared across calls: its
# options would otherwise leak between calls that omit them, and
# concurrent calls could race on the same instance's attributes.
kwargs: dict[str, Any] = {
"coordinates": (entry.data[CONF_LATITUDE], entry.data[CONF_LONGITUDE]),
"language": entry.data.get(CONF_LANGUAGE, "English").lower(),
}
for option in PRECIP_FORECAST_OPTIONS:
if option in call.data:
kwargs[option] = call.data[option]

precip = ECPrecipForecast(**kwargs)
await precip.update()

return {
"nowcast": [
{**item, "timestamp": item["timestamp"].isoformat()}
for item in precip.nowcast
],
"hourly": [
{**item, "timestamp": item["timestamp"].isoformat()}
for item in precip.hourly
],
"metadata": precip.metadata,
}


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Environment Canada integration."""
Expand All @@ -56,3 +112,10 @@ def async_setup_services(hass: HomeAssistant) -> None:
schema=SERVICE_GET_ALERTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_PRECIPITATION_FORECAST,
_async_get_precipitation_forecast,
schema=SERVICE_GET_PRECIPITATION_FORECAST_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
33 changes: 33 additions & 0 deletions homeassistant/components/environment_canada/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,39 @@ get_forecasts:
integration: environment_canada
domain: weather

get_precipitation_forecast:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: environment_canada
precip_type:
selector:
select:
options:
- "auto"
- "rain"
- "snow"
past_minutes:
selector:
number:
min: 0
max: 180
unit_of_measurement: min
future_minutes:
selector:
number:
min: 0
max: 72
unit_of_measurement: min
hourly_hours:
selector:
number:
min: 0
max: 48
unit_of_measurement: h

set_radar_type:
target:
entity:
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/environment_canada/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,32 @@
"description": "Retrieves the forecast from selected weather services.",
"name": "Get forecasts"
},
"get_precipitation_forecast": {
"description": "Retrieves the precipitation forecast series from the selected weather service.",
"fields": {
"config_entry_id": {
"description": "The Environment Canada service to retrieve the precipitation forecast from.",
"name": "Environment Canada service"
},
"future_minutes": {
"description": "How far ahead the short-interval series reaches.",
"name": "Future minutes"
},
"hourly_hours": {
"description": "Length of the hourly series. Zero disables it.",
"name": "Hourly hours"
},
"past_minutes": {
"description": "How far back the short-interval series reaches.",
"name": "Past minutes"
},
"precip_type": {
"description": "Which precipitation type to report. Automatically detects it from radar.",
"name": "Precipitation type"
}
},
"name": "Get precipitation forecast"
},
"set_radar_type": {
"description": "Sets the type of radar image to retrieve.",
"fields": {
Expand Down
31 changes: 31 additions & 0 deletions homeassistant/components/greencell/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Diagnostics support for the Greencell integration."""

from dataclasses import asdict
from typing import Any

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant

from .const import CONF_SERIAL_NUMBER
from .models import GreencellConfigEntry

TO_REDACT = {CONF_SERIAL_NUMBER}


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

return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"access": {
"disabled": runtime_data.access.is_disabled(),
"can_execute": runtime_data.access.can_execute(),
},
"current": asdict(runtime_data.current_data),
"voltage": asdict(runtime_data.voltage_data),
"power": asdict(runtime_data.power_data),
"state": asdict(runtime_data.state_data),
}
2 changes: 1 addition & 1 deletion homeassistant/components/greencell/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ rules:

# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: done
Expand Down
41 changes: 28 additions & 13 deletions homeassistant/components/intent/timers.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,20 +457,8 @@ def _timer_finished(self, timer_id: str) -> None:
timer.finish()

if timer.conversation_command:
from homeassistant.components.conversation import ( # noqa: PLC0415
async_converse,
)

self.hass.async_create_background_task(
async_converse(
self.hass,
timer.conversation_command,
conversation_id=None,
context=Context(),
language=timer.language,
agent_id=timer.conversation_agent_id,
device_id=timer.device_id,
),
self._async_run_conversation_command(timer),
"timer assist command",
)
elif timer.device_id in self.handlers:
Expand All @@ -483,6 +471,33 @@ def _timer_finished(self, timer_id: str) -> None:
timer.device_id,
)

async def _async_run_conversation_command(self, timer: TimerInfo) -> None:
"""Run the delayed command of a finished timer."""
from homeassistant.components.conversation import ( # noqa: PLC0415
async_converse,
)

assert timer.conversation_command is not None

result = await async_converse(
self.hass,
timer.conversation_command,
conversation_id=None,
context=Context(),
language=timer.language,
agent_id=timer.conversation_agent_id,
device_id=timer.device_id,
)

# Nothing is listening to the response, so an error is only visible here.
if result.response.response_type is intent.IntentResponseType.ERROR:
_LOGGER.warning(
"Delayed command failed: command=%s, code=%s, response=%s",
timer.conversation_command,
result.response.error_code,
result.response.speech.get("plain", {}).get("speech", ""),
)

def is_timer_device(self, device_id: str) -> bool:
"""Return True if device has been registered to handle timer events."""
return device_id in self.handlers
Expand Down
30 changes: 17 additions & 13 deletions homeassistant/components/knx/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@
build_yaml_unique_id,
)
from .knx_module import KNXModule
from .storage.const import CONF_ENTITY, CONF_GA_SENSOR
from .storage.util import ConfigExtractor
from .storage.config_store import KnxEntityData
from .storage.const import CONF_ENTITY
from .storage.entity_store_schema import BinarySensorKnxConfig


async def async_setup_entry(
Expand All @@ -68,7 +69,9 @@ async def async_setup_entry(
KnxYamlBinarySensor(knx_module, entity_config)
for entity_config in yaml_platform_config
)
if ui_config := knx_module.config_store.get_entity_configs(Platform.BINARY_SENSOR):
if ui_config := knx_module.config_store.get_entity_configs(
Platform.BINARY_SENSOR, BinarySensorKnxConfig
):
entities.extend(
KnxUiBinarySensor(knx_module, unique_id, config)
for unique_id, config in ui_config.items()
Expand Down Expand Up @@ -146,26 +149,27 @@ class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
_device: XknxBinarySensor

def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
self,
knx_module: KNXModule,
unique_id: str,
config: KnxEntityData[BinarySensorKnxConfig],
) -> None:
"""Initialize KNX binary sensor."""
super().__init__(
knx_module=knx_module,
unique_id=unique_id,
entity_config=config[CONF_ENTITY],
)
knx_conf = ConfigExtractor(config[DOMAIN])
knx_conf = config[DOMAIN]
self._device = XknxBinarySensor(
xknx=knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address_state=knx_conf.get_state_and_passive(CONF_GA_SENSOR),
sync_state=knx_conf.get(CONF_SYNC_STATE),
invert=knx_conf.get(CONF_INVERT, default=False),
ignore_internal_state=knx_conf.get(
CONF_IGNORE_INTERNAL_STATE, default=False
),
context_timeout=knx_conf.get(CONF_CONTEXT_TIMEOUT),
reset_after=knx_conf.get(CONF_RESET_AFTER),
group_address_state=knx_conf.ga_sensor.state_and_passive(),
sync_state=knx_conf.sync_state,
invert=knx_conf.invert,
ignore_internal_state=knx_conf.ignore_internal_state,
context_timeout=knx_conf.context_timeout,
reset_after=knx_conf.reset_after,
always_callback=True,
)
self._attr_force_update = self._device.ignore_internal_state
3 changes: 2 additions & 1 deletion homeassistant/components/knx/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
build_yaml_unique_id,
)
from .knx_module import KNXModule
from .storage.config_store import KnxEntityData
from .storage.const import CONF_DATA, CONF_ENTITY, CONF_GA_SEND
from .storage.util import ConfigExtractor

Expand Down Expand Up @@ -100,7 +101,7 @@ class KnxUiButton(_KnxButton, KnxUiEntity):
_device: XknxRawValue | XknxExposeSensor

def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
self, knx_module: KNXModule, unique_id: str, config: KnxEntityData[Any]
) -> None:
"""Initialize a KNX button."""
knx_conf = ConfigExtractor(config[DOMAIN])
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/knx/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
)
from .knx_module import KNXModule
from .schema import ClimateSchema
from .storage.config_store import KnxEntityData
from .storage.const import (
CONF_ENTITY,
CONF_GA_ACTIVE,
Expand Down Expand Up @@ -698,7 +699,7 @@ class KnxUiClimate(_KnxClimate, KnxUiEntity):
_device: XknxClimate

def __init__(
self, knx_module: KNXModule, unique_id: str, config: ConfigType
self, knx_module: KNXModule, unique_id: str, config: KnxEntityData[Any]
) -> None:
"""Initialize of a KNX climate device."""
super().__init__(
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/knx/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)
from .knx_module import KNXModule
from .schema import CoverSchema
from .storage.config_store import KnxEntityData
from .storage.const import (
CONF_ENTITY,
CONF_GA_ANGLE,
Expand Down Expand Up @@ -306,7 +307,7 @@ class KnxUiCover(_KnxCover, KnxUiEntity):
_device: XknxCover

def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
self, knx_module: KNXModule, unique_id: str, config: KnxEntityData[Any]
) -> None:
"""Initialize KNX cover."""
super().__init__(
Expand Down
Loading
Loading