mirror of
https://github.com/home-assistant/core.git
synced 2026-05-04 11:54:35 +02:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 394dafd980 | |||
| eba429dc54 | |||
| 89ce8478de | |||
| a4a8315376 | |||
| 3a705fd668 | |||
| dc0fc318b8 | |||
| 5ceb8537eb | |||
| d7d7782a69 | |||
| 2d4176d581 | |||
| 204e9a79c5 | |||
| ace7da2328 | |||
| dfe25ff804 | |||
| 2b44cf898e | |||
| c77ed921de | |||
| 78e13d138f | |||
| 4e394597bd | |||
| 78c2dc708c | |||
| 4c1d2e7ac8 | |||
| 7b809a8e55 | |||
| 4eea448f9d | |||
| f58882c878 | |||
| 4e6e9f35b5 | |||
| d5e9976b2c | |||
| 8d547d4599 | |||
| 94d79440a0 | |||
| d602b7d19b | |||
| fb5de55c3e | |||
| 5cf0ee936d | |||
| 7443878333 | |||
| 090d296135 | |||
| 415bfb40a7 | |||
| 7ced4e981e | |||
| b656ef4d4f | |||
| 6ea18a7b24 | |||
| a0ac9fe6c9 | |||
| 135735126a | |||
| 3bc6cf666a | |||
| 1929e103c0 | |||
| 74b49556f9 | |||
| 8d40f4d39f | |||
| eed126c6d4 | |||
| 38cd84fa5f | |||
| a28f5baeeb | |||
| f9352dfe8f | |||
| 5beff34069 | |||
| 119d4c2316 | |||
| 1e7ab07d9e | |||
| 7896e7675c | |||
| 8b415a0376 | |||
| 6a656c5d49 | |||
| 8d094bf12e | |||
| c71b6bdac9 | |||
| 57cc1f841b | |||
| d8f3778d77 | |||
| 9a8e3ad5cc | |||
| 019d33c06c | |||
| 40ebf3b2a9 | |||
| 7912c9e95c | |||
| 4bb1ea1da1 | |||
| a696ea18d3 | |||
| df96b94985 | |||
| 0f8ed4e73d | |||
| 34477d3559 | |||
| 96ac566032 | |||
| 87f48b15d1 | |||
| a1f2140ed7 | |||
| db7a9321be | |||
| ebb0a453f4 | |||
| 7da10794a8 | |||
| 461f0865af | |||
| fc83bb1737 | |||
| b28cdcfc49 | |||
| 3f70e2b6f0 | |||
| ed22e98861 | |||
| 093f07c04e | |||
| b5693ca604 | |||
| 20b77aa15f | |||
| 1cbd3ab930 | |||
| 31b44b7846 | |||
| de3a0841d8 | |||
| 581fb2f9f4 | |||
| 5bb4e4f5d9 | |||
| cfa619b67e | |||
| 56db7fc7dc | |||
| 1f6be7b4d1 | |||
| 52d1432d81 | |||
| 14da1e9b23 | |||
| d6e1d05e87 | |||
| 62f73cfcca | |||
| 6e9a53d02e | |||
| 394c13af1d | |||
| 86b13e8ae3 | |||
| 5a7332a135 | |||
| 0f9a91d369 | |||
| 00dd86fb4b | |||
| 460909a7f6 | |||
| 21fd012447 | |||
| c27f0c560e | |||
| 0f4a1b421e | |||
| 5e35ce2996 | |||
| e5804307e7 | |||
| 3b74b63b23 | |||
| 06df32d9d4 | |||
| 63947e4980 | |||
| ac6a377478 | |||
| 18af423a78 | |||
| f1445bc8f5 | |||
| 3784c99305 | |||
| 0084d6c5bd |
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
|
||||
@@ -37,8 +37,8 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.6"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
ALL_PYTHON_VERSIONS: "['3.12.3']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
|
||||
@@ -163,7 +163,6 @@ homeassistant.components.easyenergy.*
|
||||
homeassistant.components.ecovacs.*
|
||||
homeassistant.components.ecowitt.*
|
||||
homeassistant.components.efergy.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
|
||||
@@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @bdraco
|
||||
/tests/components/unifiprotect/ @bdraco
|
||||
/homeassistant/components/upb/ @gwww
|
||||
/tests/components/upb/ @gwww
|
||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||
|
||||
@@ -134,8 +134,15 @@ COOLDOWN_TIME = 60
|
||||
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
||||
|
||||
# Core integrations are unconditionally loaded
|
||||
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
||||
LOGGING_INTEGRATIONS = {
|
||||
|
||||
# Integrations that are loaded right after the core is set up
|
||||
LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
||||
# isal is loaded right away before `http` to ensure if its
|
||||
# enabled, that `isal` is up to date.
|
||||
"isal",
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Error logging
|
||||
@@ -214,8 +221,8 @@ CRITICAL_INTEGRATIONS = {
|
||||
}
|
||||
|
||||
SETUP_ORDER = (
|
||||
# Load logging as soon as possible
|
||||
("logging", LOGGING_INTEGRATIONS),
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||
# Setup frontend and recorder
|
||||
("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
|
||||
@@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
| AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "airgradient",
|
||||
"name": "Airgradient",
|
||||
"name": "AirGradient",
|
||||
"codeowners": ["@airgradienthq", "@joostlek"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
|
||||
@@ -103,6 +103,7 @@ SENSOR_TYPES: tuple[AirGradientSensorEntityDescription, ...] = (
|
||||
AirGradientSensorEntityDescription(
|
||||
key="pm003",
|
||||
translation_key="pm003_count",
|
||||
native_unit_of_measurement="particles/dL",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda status: status.pm003_count,
|
||||
),
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"name": "Nitrogen index"
|
||||
},
|
||||
"pm003_count": {
|
||||
"name": "PM0.3 count"
|
||||
"name": "PM0.3"
|
||||
},
|
||||
"raw_total_volatile_organic_component": {
|
||||
"name": "Raw total VOC"
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
|
||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
||||
|
||||
@@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
|
||||
Adds an empty filter to hass data.
|
||||
Tries to get a filter from yaml, if present set to hass data.
|
||||
If config is empty after getting the filter, return, otherwise emit
|
||||
deprecated warning and pass the rest to the config flow.
|
||||
"""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: {}})
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
if DOMAIN in yaml_config:
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER]
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -207,6 +206,6 @@ class AzureDataExplorer:
|
||||
if "\n" in state.state:
|
||||
return None, dropped + 1
|
||||
|
||||
json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
|
||||
json_event = json.dumps(obj=state, cls=JSONEncoder)
|
||||
|
||||
return (json_event, dropped)
|
||||
|
||||
@@ -23,7 +23,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -35,7 +35,6 @@ class AzureDataExplorerClient:
|
||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
||||
"""Create the right class."""
|
||||
|
||||
self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI]
|
||||
self._database = data[CONF_ADX_DATABASE_NAME]
|
||||
self._table = data[CONF_ADX_TABLE_NAME]
|
||||
self._ingestion_properties = IngestionProperties(
|
||||
@@ -45,24 +44,36 @@ class AzureDataExplorerClient:
|
||||
ingestion_mapping_reference="ha_json_mapping",
|
||||
)
|
||||
|
||||
# Create cLient for ingesting and querying data
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
self._cluster_ingest_uri,
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
# Create client for ingesting data
|
||||
kcsb_ingest = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI],
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
if data[CONF_USE_FREE] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb)
|
||||
# Create client for querying data
|
||||
kcsb_query = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""),
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
self.query_client = KustoClient(kcsb)
|
||||
if data[CONF_USE_QUEUED_CLIENT] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb_ingest)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
|
||||
|
||||
self.query_client = KustoClient(kcsb_query)
|
||||
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception when it cannot connect."""
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
query = f"{self._table} | take 1"
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers.selector import BooleanSelector
|
||||
|
||||
from . import AzureDataExplorerClient
|
||||
from .const import (
|
||||
@@ -19,7 +20,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
DEFAULT_OPTIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_APP_REG_ID): str,
|
||||
vol.Required(CONF_APP_REG_SECRET): str,
|
||||
vol.Required(CONF_AUTHORITY_ID): str,
|
||||
vol.Optional(CONF_USE_FREE, default=False): bool,
|
||||
vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id"
|
||||
CONF_SEND_INTERVAL = "send_interval"
|
||||
CONF_MAX_DELAY = "max_delay"
|
||||
CONF_FILTER = DATA_FILTER = "filter"
|
||||
CONF_USE_FREE = "use_queued_ingestion"
|
||||
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
|
||||
DATA_HUB = "hub"
|
||||
STEP_USER = "user"
|
||||
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Setup your Azure Data Explorer integration",
|
||||
"description": "Enter connection details.",
|
||||
"description": "Enter connection details",
|
||||
"data": {
|
||||
"clusteringesturi": "Cluster Ingest URI",
|
||||
"database": "Database name",
|
||||
"table": "Table name",
|
||||
"cluster_ingest_uri": "Cluster Ingest URI",
|
||||
"authority_id": "Authority ID",
|
||||
"client_id": "Client ID",
|
||||
"client_secret": "Client secret",
|
||||
"authority_id": "Authority ID"
|
||||
"database": "Database name",
|
||||
"table": "Table name",
|
||||
"use_queued_ingestion": "Use queued ingestion"
|
||||
},
|
||||
"data_description": {
|
||||
"cluster_ingest_uri": "Ingest-URI of the cluster",
|
||||
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ class BlinkSyncModuleHA(
|
||||
"""Representation of a Blink Alarm Control Panel."""
|
||||
|
||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
_attr_code_arm_required = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/buienradar",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["buienradar", "vincenty"],
|
||||
"requirements": ["buienradar==1.0.5"]
|
||||
"requirements": ["buienradar==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import DOMAIN, ClimateEntity
|
||||
from . import DOMAIN
|
||||
|
||||
INTENT_GET_TEMPERATURE = "HassClimateGetTemperature"
|
||||
|
||||
@@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
|
||||
intent_type = INTENT_GET_TEMPERATURE
|
||||
description = "Gets the current temperature of a climate device or entity"
|
||||
slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str}
|
||||
slot_schema = {
|
||||
vol.Optional("area"): intent.non_empty_string,
|
||||
vol.Optional("name"): intent.non_empty_string,
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
@@ -31,74 +33,24 @@ class GetTemperatureIntent(intent.IntentHandler):
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
|
||||
component: EntityComponent[ClimateEntity] = hass.data[DOMAIN]
|
||||
entities: list[ClimateEntity] = list(component.entities)
|
||||
climate_entity: ClimateEntity | None = None
|
||||
climate_state: State | None = None
|
||||
name: str | None = None
|
||||
if "name" in slots:
|
||||
name = slots["name"]["value"]
|
||||
|
||||
if not entities:
|
||||
raise intent.IntentHandleError("No climate entities")
|
||||
area: str | None = None
|
||||
if "area" in slots:
|
||||
area = slots["area"]["value"]
|
||||
|
||||
name_slot = slots.get("name", {})
|
||||
entity_name: str | None = name_slot.get("value")
|
||||
entity_text: str | None = name_slot.get("text")
|
||||
|
||||
area_slot = slots.get("area", {})
|
||||
area_id = area_slot.get("value")
|
||||
|
||||
if area_id:
|
||||
# Filter by area and optionally name
|
||||
area_name = area_slot.get("text")
|
||||
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.NoStatesMatchedError(
|
||||
reason=intent.MatchFailedReason.AREA,
|
||||
name=entity_text or entity_name,
|
||||
area=area_name or area_id,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
elif entity_name:
|
||||
# Filter by name
|
||||
for maybe_climate in intent.async_match_states(
|
||||
hass, name=entity_name, domains=[DOMAIN]
|
||||
):
|
||||
climate_state = maybe_climate
|
||||
break
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.NoStatesMatchedError(
|
||||
reason=intent.MatchFailedReason.NAME,
|
||||
name=entity_name,
|
||||
area=None,
|
||||
floor=None,
|
||||
domains={DOMAIN},
|
||||
device_classes=None,
|
||||
)
|
||||
|
||||
climate_entity = component.get_entity(climate_state.entity_id)
|
||||
else:
|
||||
# First entity
|
||||
climate_entity = entities[0]
|
||||
climate_state = hass.states.get(climate_entity.entity_id)
|
||||
|
||||
assert climate_entity is not None
|
||||
|
||||
if climate_state is None:
|
||||
raise intent.IntentHandleError(f"No state for {climate_entity.name}")
|
||||
|
||||
assert climate_state is not None
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
||||
response.async_set_states(matched_states=[climate_state])
|
||||
response.async_set_states(matched_states=match_result.states)
|
||||
return response
|
||||
|
||||
@@ -86,6 +86,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
|
||||
|
||||
self._attr_name = name
|
||||
self._code = code
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._mode = mode
|
||||
self._url = url
|
||||
self._alarm = concord232_client.Client(self._url)
|
||||
|
||||
@@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
director_all_items = json.loads(director_all_items)
|
||||
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
|
||||
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration())
|
||||
# Check if OS version is 3 or higher to get UI configuration
|
||||
entry_data[CONF_UI_CONFIGURATION] = None
|
||||
if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3:
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(
|
||||
await director.getUiConfiguration()
|
||||
)
|
||||
|
||||
# Load options from config entry
|
||||
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(
|
||||
|
||||
@@ -81,11 +81,18 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Control4 rooms from a config entry."""
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
# OS 2 will not have a ui_configuration
|
||||
if not ui_config:
|
||||
_LOGGER.debug("No UI Configuration found for Control4")
|
||||
return
|
||||
|
||||
all_rooms = await get_rooms(hass, entry)
|
||||
if not all_rooms:
|
||||
return
|
||||
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
scan_interval = entry_data[CONF_SCAN_INTERVAL]
|
||||
_LOGGER.debug("Scan interval = %s", scan_interval)
|
||||
|
||||
@@ -119,8 +126,6 @@ async def async_setup_entry(
|
||||
if "parentId" in item and k > 1
|
||||
}
|
||||
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
entity_list = []
|
||||
for room in all_rooms:
|
||||
room_id = room["id"]
|
||||
|
||||
@@ -429,8 +429,15 @@ class DefaultAgent(ConversationEntity):
|
||||
intent_context=intent_context,
|
||||
language=language,
|
||||
):
|
||||
if ("name" in result.entities) and (
|
||||
not result.entities["name"].is_wildcard
|
||||
# Prioritize results with a "name" slot, but still prefer ones with
|
||||
# more literal text matched.
|
||||
if (
|
||||
("name" in result.entities)
|
||||
and (not result.entities["name"].is_wildcard)
|
||||
and (
|
||||
(name_result is None)
|
||||
or (result.text_chunks_matched > name_result.text_chunks_matched)
|
||||
)
|
||||
):
|
||||
name_result = result
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.3"]
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.5"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ class EcobeeNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message and raise issue."""
|
||||
migrate_notify_issue(self.hass, DOMAIN, "Ecobee", "2024.11.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "Ecobee", "2024.11.0", service_name=self._service_name
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.send_message, message, **kwargs)
|
||||
)
|
||||
|
||||
@@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
_attr_state: str | None
|
||||
_attr_code_arm_required = False
|
||||
_attr_supported_features = (
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyElectra==1.2.0"]
|
||||
"requirements": ["pyElectra==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity):
|
||||
self._attr_unique_id = coordinator.data.info.serial_number
|
||||
|
||||
# Elgato Light supporting color, have a different temperature range
|
||||
if self.coordinator.data.settings.power_on_hue is not None:
|
||||
if (
|
||||
self.coordinator.data.info.product_name
|
||||
in (
|
||||
"Elgato Light Strip",
|
||||
"Elgato Light Strip Pro",
|
||||
)
|
||||
or self.coordinator.data.settings.power_on_hue
|
||||
or self.coordinator.data.state.hue is not None
|
||||
):
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
self._attr_min_mireds = 153
|
||||
self._attr_max_mireds = 285
|
||||
|
||||
@@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
self._device: OpenWebIfDevice = device
|
||||
self._entry = entry
|
||||
|
||||
self._attr_unique_id = device.mac_address
|
||||
self._attr_unique_id = device.mac_address or entry.entry_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.mac_address)},
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
manufacturer=about["info"]["brand"],
|
||||
model=about["info"]["model"],
|
||||
configuration_url=device.base,
|
||||
|
||||
@@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
):
|
||||
"""Initialize the alarm panel."""
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._attr_code_format = CodeFormat.NUMBER
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
@@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self) -> CodeFormat | None:
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return CodeFormat.NUMBER
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
@@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
@@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
self.hass.data[DATA_EVL].arm_night_partition(
|
||||
str(code) if code else str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number)
|
||||
|
||||
@callback
|
||||
def async_alarm_keypress(self, keypress=None):
|
||||
|
||||
@@ -69,7 +69,9 @@ class FileNotificationService(BaseNotificationService):
|
||||
"""Send a message to a file."""
|
||||
# The use of the legacy notify service was deprecated with HA Core 2024.6.0
|
||||
# and will be removed with HA Core 2024.12
|
||||
migrate_notify_issue(self.hass, DOMAIN, "File", "2024.12.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "File", "2024.12.0", service_name=self._service_name
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(self.send_message, message, **kwargs)
|
||||
)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240604.0"]
|
||||
"requirements": ["home-assistant-frontend==20240610.1"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["gardena-bluetooth==1.4.1"]
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/glances",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["glances_api"],
|
||||
"requirements": ["glances-api==0.7.0"]
|
||||
"requirements": ["glances-api==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/goodwe",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.3.5"]
|
||||
"requirements": ["goodwe==0.3.6"]
|
||||
}
|
||||
|
||||
@@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait):
|
||||
if features & required_feature != 0
|
||||
]
|
||||
|
||||
def _default_arm_state(self):
|
||||
states = self._supported_states()
|
||||
|
||||
if STATE_ALARM_TRIGGERED in states:
|
||||
states.remove(STATE_ALARM_TRIGGERED)
|
||||
|
||||
if len(states) != 1:
|
||||
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
|
||||
|
||||
return states[0]
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return ArmDisarm attributes for a sync request."""
|
||||
response = {}
|
||||
@@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait):
|
||||
def query_attributes(self):
|
||||
"""Return ArmDisarm query attributes."""
|
||||
armed_state = self.state.attributes.get("next_state", self.state.state)
|
||||
response = {"isArmed": armed_state in self.state_to_service}
|
||||
if response["isArmed"]:
|
||||
response.update({"currentArmLevel": armed_state})
|
||||
return response
|
||||
|
||||
if armed_state in self.state_to_service:
|
||||
return {"isArmed": True, "currentArmLevel": armed_state}
|
||||
return {
|
||||
"isArmed": False,
|
||||
"currentArmLevel": self._default_arm_state(),
|
||||
}
|
||||
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an ArmDisarm command."""
|
||||
@@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait):
|
||||
# If no arm level given, we can only arm it if there is
|
||||
# only one supported arm type. We never default to triggered.
|
||||
if not (arm_level := params.get("armLevel")):
|
||||
states = self._supported_states()
|
||||
|
||||
if STATE_ALARM_TRIGGERED in states:
|
||||
states.remove(STATE_ALARM_TRIGGERED)
|
||||
|
||||
if len(states) != 1:
|
||||
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
|
||||
|
||||
arm_level = states[0]
|
||||
arm_level = self._default_arm_state()
|
||||
|
||||
if self.state.state == arm_level:
|
||||
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")
|
||||
|
||||
@@ -165,7 +165,7 @@ class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
|
||||
await session.async_ensure_token_valid()
|
||||
self.assistant = None
|
||||
if not self.assistant or user_input.language != self.language:
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
self.language = user_input.language
|
||||
self.assistant = TextAssistant(credentials, self.language)
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_send_text_commands(
|
||||
entry.async_start_reauth(hass)
|
||||
raise
|
||||
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN])
|
||||
credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass))
|
||||
with TextAssistant(
|
||||
credentials, language_code, audio_out=bool(media_players)
|
||||
|
||||
@@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
try:
|
||||
response = await model.generate_content_async(prompt_parts)
|
||||
except (
|
||||
ClientError,
|
||||
GoogleAPICallError,
|
||||
ValueError,
|
||||
genai_types.BlockedPromptException,
|
||||
genai_types.StopCandidateException,
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
from typing import Any, Literal
|
||||
|
||||
import google.ai.generativelanguage as glm
|
||||
from google.api_core.exceptions import GoogleAPICallError
|
||||
import google.generativeai as genai
|
||||
from google.generativeai import protos
|
||||
import google.generativeai.types as genai_types
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
import voluptuous as vol
|
||||
@@ -93,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
|
||||
parameters = _format_schema(convert(tool.parameters))
|
||||
|
||||
return glm.Tool(
|
||||
return protos.Tool(
|
||||
{
|
||||
"function_declarations": [
|
||||
{
|
||||
@@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
def _adjust_value(value: Any) -> Any:
|
||||
"""Reverse unnecessary single quotes escaping."""
|
||||
def _escape_decode(value: Any) -> Any:
|
||||
"""Recursively call codecs.escape_decode on all values."""
|
||||
if isinstance(value, str):
|
||||
return value.replace("\\'", "'")
|
||||
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
|
||||
if isinstance(value, list):
|
||||
return [_adjust_value(item) for item in value]
|
||||
return [_escape_decode(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {k: _adjust_value(v) for k, v in value.items()}
|
||||
return {k: _escape_decode(v) for k, v in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
@@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
for function_call in function_calls:
|
||||
tool_call = MessageToDict(function_call._pb) # noqa: SLF001
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = {
|
||||
key: _adjust_value(value)
|
||||
for key, value in tool_call["args"].items()
|
||||
}
|
||||
tool_args = _escape_decode(tool_call["args"])
|
||||
LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args)
|
||||
tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
try:
|
||||
@@ -349,13 +347,13 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
LOGGER.debug("Tool response: %s", function_response)
|
||||
tool_responses.append(
|
||||
glm.Part(
|
||||
function_response=glm.FunctionResponse(
|
||||
protos.Part(
|
||||
function_response=protos.FunctionResponse(
|
||||
name=tool_name, response=function_response
|
||||
)
|
||||
)
|
||||
)
|
||||
chat_request = glm.Content(parts=tool_responses)
|
||||
chat_request = protos.Content(parts=tool_responses)
|
||||
|
||||
intent_response.async_set_speech(
|
||||
" ".join([part.text.strip() for part in chat_response.parts if part.text])
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"]
|
||||
"requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
|
||||
service = Client(
|
||||
Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
)
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
|
||||
@@ -61,7 +61,9 @@ class OAuth2FlowHandler(
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]))
|
||||
service = Client(
|
||||
Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call]
|
||||
)
|
||||
|
||||
if self.reauth_entry:
|
||||
_LOGGER.debug("service.open_by_key")
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"after_dependencies": [
|
||||
"alarm_control_panel",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"plant",
|
||||
"vacuum",
|
||||
|
||||
@@ -36,7 +36,14 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity import (
|
||||
@@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
|
||||
get_unit_of_measurement,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
@@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
self._native_unit_of_measurement = unit_of_measurement
|
||||
self._valid_units: set[str | None] = set()
|
||||
self._can_convert: bool = False
|
||||
self.calculate_attributes_later: CALLBACK_TYPE | None = None
|
||||
self._attr_name = name
|
||||
if name == DEFAULT_NAME:
|
||||
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
|
||||
@@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When added to hass."""
|
||||
for entity_id in self._entity_ids:
|
||||
if self.hass.states.get(entity_id) is None:
|
||||
self.calculate_attributes_later = async_track_state_change_event(
|
||||
self.hass, self._entity_ids, self.calculate_state_attributes
|
||||
)
|
||||
break
|
||||
if not self.calculate_attributes_later:
|
||||
await self.calculate_state_attributes()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def calculate_state_attributes(
|
||||
self, event: Event[EventStateChangedData] | None = None
|
||||
) -> None:
|
||||
"""Calculate state attributes."""
|
||||
for entity_id in self._entity_ids:
|
||||
if self.hass.states.get(entity_id) is None:
|
||||
return
|
||||
if self.calculate_attributes_later:
|
||||
self.calculate_attributes_later()
|
||||
self.calculate_attributes_later = None
|
||||
self._attr_state_class = self._calculate_state_class(self._state_class)
|
||||
self._attr_device_class = self._calculate_device_class(self._device_class)
|
||||
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
|
||||
self._native_unit_of_measurement
|
||||
)
|
||||
self._valid_units = self._get_valid_units()
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def async_update_group_state(self) -> None:
|
||||
|
||||
@@ -267,15 +267,14 @@ class SupervisorIssues:
|
||||
placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
|
||||
|
||||
if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
|
||||
f"/hassio/addon/{issue.reference}"
|
||||
)
|
||||
addons = get_addons_info(self._hass)
|
||||
if addons and issue.reference in addons:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
|
||||
"name"
|
||||
]
|
||||
if "url" in addons[issue.reference]:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[
|
||||
issue.reference
|
||||
]["url"]
|
||||
else:
|
||||
placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
| AlarmControlPanelEntityFeature.TRIGGER
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.49", "babel==2.13.1"]
|
||||
"requirements": ["holidays==0.50", "babel==2.13.1"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"domain": "http",
|
||||
"name": "HTTP",
|
||||
"after_dependencies": ["isal"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/http",
|
||||
"integration_type": "system",
|
||||
|
||||
@@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler):
|
||||
intent_type = INTENT_HUMIDITY
|
||||
description = "Set desired humidity level"
|
||||
slot_schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Required("name"): intent.non_empty_string,
|
||||
vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
@@ -44,18 +44,19 @@ class HumidityHandler(intent.IntentHandler):
|
||||
"""Handle the hass intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = list(
|
||||
intent.async_match_states(
|
||||
hass,
|
||||
name=slots["name"]["value"],
|
||||
states=hass.states.async_all(DOMAIN),
|
||||
)
|
||||
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=slots["name"]["value"],
|
||||
domains=[DOMAIN],
|
||||
assistant=intent_obj.assistant,
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
if not states:
|
||||
raise intent.IntentHandleError("No entities matched")
|
||||
|
||||
state = states[0]
|
||||
state = match_result.states[0]
|
||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||
|
||||
humidity = slots["humidity"]["value"]
|
||||
@@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler):
|
||||
intent_type = INTENT_MODE
|
||||
description = "Set humidifier mode"
|
||||
slot_schema = {
|
||||
vol.Required("name"): cv.string,
|
||||
vol.Required("name"): intent.non_empty_string,
|
||||
vol.Required("mode"): cv.string,
|
||||
}
|
||||
platforms = {DOMAIN}
|
||||
@@ -98,18 +99,18 @@ class SetModeHandler(intent.IntentHandler):
|
||||
"""Handle the hass intent."""
|
||||
hass = intent_obj.hass
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
states = list(
|
||||
intent.async_match_states(
|
||||
hass,
|
||||
name=slots["name"]["value"],
|
||||
states=hass.states.async_all(DOMAIN),
|
||||
)
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=slots["name"]["value"],
|
||||
domains=[DOMAIN],
|
||||
assistant=intent_obj.assistant,
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
if not states:
|
||||
raise intent.IntentHandleError("No entities matched")
|
||||
|
||||
state = states[0]
|
||||
state = match_result.states[0]
|
||||
service_data = {ATTR_ENTITY_ID: state.entity_id}
|
||||
|
||||
intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes")
|
||||
|
||||
@@ -24,13 +24,17 @@ class HydrawiseBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Hydrawise binary sensor."""
|
||||
|
||||
value_fn: Callable[[HydrawiseBinarySensor], bool | None]
|
||||
always_available: bool = False
|
||||
|
||||
|
||||
CONTROLLER_BINARY_SENSORS: tuple[HydrawiseBinarySensorEntityDescription, ...] = (
|
||||
HydrawiseBinarySensorEntityDescription(
|
||||
key="status",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success,
|
||||
value_fn=lambda status_sensor: status_sensor.coordinator.last_update_success
|
||||
and status_sensor.controller.online,
|
||||
# Connectivtiy sensor is always available
|
||||
always_available=True,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -98,3 +102,10 @@ class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update state attributes."""
|
||||
self._attr_is_on = self.entity_description.value_fn(self)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Set the entity availability."""
|
||||
if self.entity_description.always_available:
|
||||
return True
|
||||
return super().available
|
||||
|
||||
@@ -70,3 +70,8 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
self.controller = self.coordinator.data.controllers[self.controller.id]
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Set the entity availability."""
|
||||
return super().available and self.controller.online
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2024.6.2"]
|
||||
"requirements": ["pydrawise==2024.6.3"]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class IAlarmPanel(
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None:
|
||||
"""Create the entity with a DataUpdateCoordinator."""
|
||||
|
||||
@@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
desk = Desk(None, monitor_height=False)
|
||||
try:
|
||||
await desk.connect(discovery_info.device, auto_reconnect=False)
|
||||
await desk.connect(discovery_info.device, retry=False)
|
||||
except AuthFailedError:
|
||||
errors["base"] = "auth_failed"
|
||||
except TimeoutError:
|
||||
|
||||
@@ -195,13 +195,13 @@ class ImapMessage:
|
||||
):
|
||||
message_untyped_text = str(part.get_payload())
|
||||
|
||||
if message_text is not None:
|
||||
if message_text is not None and message_text.strip():
|
||||
return message_text
|
||||
|
||||
if message_html is not None:
|
||||
if message_html:
|
||||
return message_html
|
||||
|
||||
if message_untyped_text is not None:
|
||||
if message_untyped_text:
|
||||
return message_untyped_text
|
||||
|
||||
return str(self.email_message.get_payload())
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["imgw_pib==1.0.1"]
|
||||
"requirements": ["imgw_pib==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -283,16 +283,13 @@ class KNXClimate(KnxEntity, ClimateEntity):
|
||||
)
|
||||
if knx_controller_mode in self._device.mode.controller_modes:
|
||||
await self._device.mode.set_controller_mode(knx_controller_mode)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._device.supports_on_off:
|
||||
if hvac_mode == HVACMode.OFF:
|
||||
await self._device.turn_off()
|
||||
elif not self._device.is_on:
|
||||
# for default hvac mode, otherwise above would have triggered
|
||||
await self._device.turn_on()
|
||||
self.async_write_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
|
||||
@@ -60,7 +60,9 @@ class KNXNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a notification to knx bus."""
|
||||
migrate_notify_issue(self.hass, DOMAIN, "KNX", "2024.11.0")
|
||||
migrate_notify_issue(
|
||||
self.hass, DOMAIN, "KNX", "2024.11.0", service_name=self._service_name
|
||||
)
|
||||
if "target" in kwargs:
|
||||
await self._async_send_to_device(message, kwargs["target"])
|
||||
else:
|
||||
|
||||
@@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity):
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(
|
||||
self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str
|
||||
|
||||
@@ -57,7 +57,7 @@ class AsyncConfigEntryAuth(AbstractAuth):
|
||||
# even when it is expired to fully hand off this responsibility and
|
||||
# know it is working at startup (then if not, fail loudly).
|
||||
token = self._oauth_session.token
|
||||
creds = Credentials(
|
||||
creds = Credentials( # type: ignore[no-untyped-call]
|
||||
token=token["access_token"],
|
||||
refresh_token=token["refresh_token"],
|
||||
token_uri=OAUTH2_TOKEN,
|
||||
@@ -92,7 +92,7 @@ class AccessTokenAuthImpl(AbstractAuth):
|
||||
|
||||
async def async_get_creds(self) -> Credentials:
|
||||
"""Return an OAuth credential for Pub/Sub Subscriber."""
|
||||
return Credentials(
|
||||
return Credentials( # type: ignore[no-untyped-call]
|
||||
token=self._access_token,
|
||||
token_uri=OAUTH2_TOKEN,
|
||||
scopes=SDM_SCOPES,
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==4.0.4"]
|
||||
"requirements": ["google-nest-sdm==4.0.5"]
|
||||
}
|
||||
|
||||
@@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the zone."""
|
||||
await self.async_set_hvac_mode(OPERATION_MODE_OFF)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
self._signal_zone_update()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on the zone."""
|
||||
await self.async_set_hvac_mode(OPERATION_MODE_AUTO)
|
||||
await self.async_set_hvac_mode(HVACMode.AUTO)
|
||||
self._signal_zone_update()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
|
||||
@@ -12,9 +12,31 @@ from .const import DOMAIN
|
||||
|
||||
@callback
|
||||
def migrate_notify_issue(
|
||||
hass: HomeAssistant, domain: str, integration_title: str, breaks_in_ha_version: str
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
integration_title: str,
|
||||
breaks_in_ha_version: str,
|
||||
service_name: str | None = None,
|
||||
) -> None:
|
||||
"""Ensure an issue is registered."""
|
||||
if service_name is not None:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"migrate_notify_{domain}_{service_name}",
|
||||
breaks_in_ha_version=breaks_in_ha_version,
|
||||
issue_domain=domain,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
translation_key="migrate_notify_service",
|
||||
translation_placeholders={
|
||||
"domain": domain,
|
||||
"integration_title": integration_title,
|
||||
"service_name": service_name,
|
||||
},
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
)
|
||||
return
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
||||
@@ -72,6 +72,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"migrate_notify_service": {
|
||||
"title": "Legacy service `notify.{service_name}` stll being used",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The {integration_title} `notify.{service_name}` service is migrated, but it seems the old `notify` service is still being used.\n\nA new `notify` entity is available now to replace each legacy `notify` service.\n\nUpdate any automations or scripts to use the new `notify.send_message` service exposed with this new entity. When this is done, select Submit and restart Home Assistant.",
|
||||
"title": "Migrate legacy {integration_title} notify service for domain `{domain}`"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ class NX584Alarm(AlarmControlPanelEntity):
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, name: str, alarm_client: client.Client, url: str) -> None:
|
||||
"""Init the nx584 alarm panel."""
|
||||
|
||||
@@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None:
|
||||
domain=DOMAIN,
|
||||
issue_id=_get_issue_id(entry_id),
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
|
||||
translation_key="deprecated_v25",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.6"]
|
||||
"requirements": ["opower==0.4.7"]
|
||||
}
|
||||
|
||||
@@ -240,6 +240,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity
|
||||
"""Representation of an Overkiz Alarm Control Panel."""
|
||||
|
||||
entity_description: OverkizAlarmDescription
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -59,9 +59,17 @@ class PingDataICMPLib(PingData):
|
||||
privileged=self._privileged,
|
||||
)
|
||||
except NameLookupError:
|
||||
_LOGGER.debug("Error resolving host: %s", self.ip_address)
|
||||
self.is_alive = False
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"async_ping returned: reachable=%s sent=%i received=%s",
|
||||
data.is_alive,
|
||||
data.packets_sent,
|
||||
data.packets_received,
|
||||
)
|
||||
|
||||
self.is_alive = data.is_alive
|
||||
if not self.is_alive:
|
||||
self.data = None
|
||||
@@ -94,6 +102,10 @@ class PingDataSubProcess(PingData):
|
||||
|
||||
async def async_ping(self) -> dict[str, Any] | None:
|
||||
"""Send ICMP echo request and return details if success."""
|
||||
_LOGGER.debug(
|
||||
"Pinging %s with: `%s`", self.ip_address, " ".join(self._ping_cmd)
|
||||
)
|
||||
|
||||
pinger = await asyncio.create_subprocess_exec(
|
||||
*self._ping_cmd,
|
||||
stdin=None,
|
||||
@@ -141,18 +153,20 @@ class PingDataSubProcess(PingData):
|
||||
assert match is not None
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
except TimeoutError:
|
||||
_LOGGER.exception(
|
||||
"Timed out running command: `%s`, after: %ss",
|
||||
self._ping_cmd,
|
||||
_LOGGER.debug(
|
||||
"Timed out running command: `%s`, after: %s",
|
||||
" ".join(self._ping_cmd),
|
||||
self._count + PING_TIMEOUT,
|
||||
)
|
||||
|
||||
if pinger:
|
||||
with suppress(TypeError):
|
||||
await pinger.kill() # type: ignore[func-returns-value]
|
||||
del pinger
|
||||
|
||||
return None
|
||||
except AttributeError:
|
||||
except AttributeError as err:
|
||||
_LOGGER.debug("Error matching ping output: %s", err)
|
||||
return None
|
||||
return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity):
|
||||
"""The platform class required by Home Assistant."""
|
||||
|
||||
_attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, point_client: MinutPointClient, home_id: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
|
||||
from radios import FilterBy, Order, RadioBrowser, Station
|
||||
from radios.radio_browser import pycountry
|
||||
|
||||
from homeassistant.components.media_player import MediaClass, MediaType
|
||||
from homeassistant.components.media_source.error import Unresolvable
|
||||
@@ -145,6 +146,8 @@ class RadioMediaSource(MediaSource):
|
||||
|
||||
# We show country in the root additionally, when there is no item
|
||||
if not item.identifier or category == "country":
|
||||
# Trigger the lazy loading of the country database to happen inside the executor
|
||||
await self.hass.async_add_executor_job(lambda: len(pycountry.countries))
|
||||
countries = await radios.countries(order=Order.NAME)
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
|
||||
@@ -1245,7 +1245,7 @@ def _first_statistic(
|
||||
table: type[StatisticsBase],
|
||||
metadata_id: int,
|
||||
) -> datetime | None:
|
||||
"""Return the data of the oldest statistic row for a given metadata id."""
|
||||
"""Return the date of the oldest statistic row for a given metadata id."""
|
||||
stmt = lambda_stmt(
|
||||
lambda: select(table.start_ts)
|
||||
.filter(table.metadata_id == metadata_id)
|
||||
@@ -1257,12 +1257,30 @@ def _first_statistic(
|
||||
return None
|
||||
|
||||
|
||||
def _last_statistic(
|
||||
session: Session,
|
||||
table: type[StatisticsBase],
|
||||
metadata_id: int,
|
||||
) -> datetime | None:
|
||||
"""Return the date of the newest statistic row for a given metadata id."""
|
||||
stmt = lambda_stmt(
|
||||
lambda: select(table.start_ts)
|
||||
.filter(table.metadata_id == metadata_id)
|
||||
.order_by(table.start_ts.desc())
|
||||
.limit(1)
|
||||
)
|
||||
if stats := cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)):
|
||||
return dt_util.utc_from_timestamp(stats[0].start_ts)
|
||||
return None
|
||||
|
||||
|
||||
def _get_oldest_sum_statistic(
|
||||
session: Session,
|
||||
head_start_time: datetime | None,
|
||||
main_start_time: datetime | None,
|
||||
tail_start_time: datetime | None,
|
||||
oldest_stat: datetime | None,
|
||||
oldest_5_min_stat: datetime | None,
|
||||
tail_only: bool,
|
||||
metadata_id: int,
|
||||
) -> float | None:
|
||||
@@ -1307,6 +1325,15 @@ def _get_oldest_sum_statistic(
|
||||
|
||||
if (
|
||||
head_start_time is not None
|
||||
and oldest_5_min_stat is not None
|
||||
and (
|
||||
# If we want stats older than the short term purge window, don't lookup
|
||||
# the oldest sum in the short term table, as it would be prioritized
|
||||
# over older LongTermStats.
|
||||
(oldest_stat is None)
|
||||
or (oldest_5_min_stat < oldest_stat)
|
||||
or (oldest_5_min_stat <= head_start_time)
|
||||
)
|
||||
and (
|
||||
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
|
||||
session, head_start_time, StatisticsShortTerm, metadata_id
|
||||
@@ -1477,13 +1504,16 @@ def statistic_during_period(
|
||||
tail_start_time: datetime | None = None
|
||||
tail_end_time: datetime | None = None
|
||||
if end_time is None:
|
||||
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
|
||||
tail_start_time = _last_statistic(session, Statistics, metadata_id)
|
||||
if tail_start_time:
|
||||
tail_start_time += Statistics.duration
|
||||
else:
|
||||
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
|
||||
elif tail_only:
|
||||
tail_start_time = start_time
|
||||
tail_end_time = end_time
|
||||
elif end_time.minute:
|
||||
tail_start_time = (
|
||||
start_time
|
||||
if tail_only
|
||||
else end_time.replace(minute=0, second=0, microsecond=0)
|
||||
)
|
||||
tail_start_time = end_time.replace(minute=0, second=0, microsecond=0)
|
||||
tail_end_time = end_time
|
||||
|
||||
# Calculate the main period
|
||||
@@ -1518,6 +1548,7 @@ def statistic_during_period(
|
||||
main_start_time,
|
||||
tail_start_time,
|
||||
oldest_stat,
|
||||
oldest_5_min_stat,
|
||||
tail_only,
|
||||
metadata_id,
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ def _register_new_account(
|
||||
|
||||
configurator.request_done(hass, request_id)
|
||||
|
||||
request_id = configurator.async_request_config(
|
||||
request_id = configurator.request_config(
|
||||
hass,
|
||||
f"{DOMAIN} - {account_name}",
|
||||
callback=register_account_callback,
|
||||
|
||||
@@ -116,7 +116,6 @@ async def async_setup_entry(
|
||||
class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
|
||||
"""An implementation of a Reolink IP camera."""
|
||||
|
||||
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
|
||||
entity_description: ReolinkCameraEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -130,6 +129,9 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
|
||||
ReolinkChannelCoordinatorEntity.__init__(self, reolink_data, channel)
|
||||
Camera.__init__(self)
|
||||
|
||||
if "snapshots" not in entity_description.stream:
|
||||
self._attr_supported_features = CameraEntityFeature.STREAM
|
||||
|
||||
if self._host.api.model in DUAL_LENS_MODELS:
|
||||
self._attr_translation_key = (
|
||||
f"{entity_description.translation_key}_lens_{self._channel}"
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, selector
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import CONF_USE_HTTPS, DOMAIN
|
||||
@@ -60,7 +60,24 @@ class ReolinkOptionsFlowHandler(OptionsFlow):
|
||||
vol.Required(
|
||||
CONF_PROTOCOL,
|
||||
default=self.config_entry.options[CONF_PROTOCOL],
|
||||
): vol.In(["rtsp", "rtmp", "flv"]),
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[
|
||||
selector.SelectOptionDict(
|
||||
value="rtsp",
|
||||
label="RTSP",
|
||||
),
|
||||
selector.SelectOptionDict(
|
||||
value="rtmp",
|
||||
label="RTMP",
|
||||
),
|
||||
selector.SelectOptionDict(
|
||||
value="flv",
|
||||
label="FLV",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -101,6 +101,11 @@ class ReolinkHostCoordinatorEntity(ReolinkBaseCoordinatorEntity[None]):
|
||||
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Force full update from the generic entity update service."""
|
||||
self._host.last_wake = 0
|
||||
await super().async_update()
|
||||
|
||||
|
||||
class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
"""Parent class for Reolink hardware camera entities connected to a channel of the NVR."""
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
from collections import defaultdict
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from time import time
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiohttp
|
||||
@@ -40,6 +41,10 @@ POLL_INTERVAL_NO_PUSH = 5
|
||||
LONG_POLL_COOLDOWN = 0.75
|
||||
LONG_POLL_ERROR_COOLDOWN = 30
|
||||
|
||||
# Conserve battery by not waking the battery cameras each minute during normal update
|
||||
# Most props are cached in the Home Hub and updated, but some are skipped
|
||||
BATTERY_WAKE_UPDATE_INTERVAL = 3600 # seconds
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -68,6 +73,7 @@ class ReolinkHost:
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
self.last_wake: float = 0
|
||||
self._update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict(
|
||||
lambda: defaultdict(int)
|
||||
)
|
||||
@@ -337,7 +343,13 @@ class ReolinkHost:
|
||||
|
||||
async def update_states(self) -> None:
|
||||
"""Call the API of the camera device to update the internal states."""
|
||||
await self._api.get_states(cmd_list=self._update_cmd)
|
||||
wake = False
|
||||
if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL:
|
||||
# wake the battery cameras for a complete update
|
||||
wake = True
|
||||
self.last_wake = time()
|
||||
|
||||
await self._api.get_states(cmd_list=self._update_cmd, wake=wake)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the API, so the connection will be released."""
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["roborock"],
|
||||
"requirements": [
|
||||
"python-roborock==2.2.2",
|
||||
"python-roborock==2.3.0",
|
||||
"vacuum-map-parser-roborock==0.1.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -297,16 +297,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
if version == 2:
|
||||
if minor_version < 2:
|
||||
# Cleanup invalid MAC addresses - see #103512
|
||||
dev_reg = dr.async_get(hass)
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
dev_reg, config_entry.entry_id
|
||||
):
|
||||
new_connections = device.connections.copy()
|
||||
new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none"))
|
||||
if new_connections != device.connections:
|
||||
dev_reg.async_update_device(
|
||||
device.id, new_connections=new_connections
|
||||
)
|
||||
# Reverted due to device registry collisions - see #119082 / #119249
|
||||
|
||||
minor_version = 2
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=2)
|
||||
|
||||
@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
except RequestError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
coordinator = SENZDataUpdateCoordinator(
|
||||
coordinator: SENZDataUpdateCoordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=account.username,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import Final
|
||||
|
||||
from aioshelly.block_device import BlockDevice
|
||||
@@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b
|
||||
entry, platforms
|
||||
):
|
||||
if shelly_entry_data.rpc:
|
||||
with contextlib.suppress(DeviceConnectionError):
|
||||
# If the device is restarting or has gone offline before
|
||||
# the ping/pong timeout happens, the shutdown command
|
||||
# will fail, but we don't care since we are unloading
|
||||
# and if we setup again, we will fix anything that is
|
||||
# in an inconsistent state at that time.
|
||||
await shelly_entry_data.rpc.shutdown()
|
||||
await shelly_entry_data.rpc.shutdown()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
@@ -584,11 +584,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
raise UpdateFailed(
|
||||
f"Sleeping device did not update within {self.sleep_period} seconds interval"
|
||||
)
|
||||
if self.device.connected:
|
||||
return
|
||||
|
||||
if not await self._async_device_connect_task():
|
||||
raise UpdateFailed("Device reconnect error")
|
||||
async with self._connection_lock:
|
||||
if self.device.connected: # Already connected
|
||||
return
|
||||
|
||||
if not await self._async_device_connect_task():
|
||||
raise UpdateFailed("Device reconnect error")
|
||||
|
||||
async def _async_disconnected(self, reconnect: bool) -> None:
|
||||
"""Handle device disconnected."""
|
||||
@@ -623,7 +625,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
if self.connected: # Already connected
|
||||
return
|
||||
self.connected = True
|
||||
await self._async_run_connected_events()
|
||||
try:
|
||||
await self._async_run_connected_events()
|
||||
except DeviceConnectionError as err:
|
||||
LOGGER.error(
|
||||
"Error running connected events for device %s: %s", self.name, err
|
||||
)
|
||||
self.last_update_success = False
|
||||
|
||||
async def _async_run_connected_events(self) -> None:
|
||||
"""Run connected events.
|
||||
@@ -697,10 +705,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
if self.device.connected:
|
||||
try:
|
||||
await async_stop_scanner(self.device)
|
||||
await super().shutdown()
|
||||
except InvalidAuthError:
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
return
|
||||
await super().shutdown()
|
||||
except DeviceConnectionError as err:
|
||||
# If the device is restarting or has gone offline before
|
||||
# the ping/pong timeout happens, the shutdown command
|
||||
# will fail, but we don't care since we are unloading
|
||||
# and if we setup again, we will fix anything that is
|
||||
# in an inconsistent state at that time.
|
||||
LOGGER.debug("Error during shutdown for device %s: %s", self.name, err)
|
||||
return
|
||||
await self._async_disconnected(False)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==10.0.0"],
|
||||
"requirements": ["aioshelly==10.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any
|
||||
import pysnmp.hlapi.asyncio as hlapi
|
||||
from pysnmp.hlapi.asyncio import (
|
||||
CommunityData,
|
||||
ObjectIdentity,
|
||||
ObjectType,
|
||||
UdpTransportTarget,
|
||||
UsmUserData,
|
||||
getCmd,
|
||||
@@ -63,7 +65,12 @@ from .const import (
|
||||
MAP_PRIV_PROTOCOLS,
|
||||
SNMP_VERSIONS,
|
||||
)
|
||||
from .util import RequestArgsType, async_create_request_cmd_args
|
||||
from .util import (
|
||||
CommandArgsType,
|
||||
RequestArgsType,
|
||||
async_create_command_cmd_args,
|
||||
async_create_request_cmd_args,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -125,23 +132,23 @@ async def async_setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the SNMP switch."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
name: str = config[CONF_NAME]
|
||||
host: str = config[CONF_HOST]
|
||||
port: int = config[CONF_PORT]
|
||||
community = config.get(CONF_COMMUNITY)
|
||||
baseoid: str = config[CONF_BASEOID]
|
||||
command_oid = config.get(CONF_COMMAND_OID)
|
||||
command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON)
|
||||
command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF)
|
||||
command_oid: str | None = config.get(CONF_COMMAND_OID)
|
||||
command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON)
|
||||
command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF)
|
||||
version: str = config[CONF_VERSION]
|
||||
username = config.get(CONF_USERNAME)
|
||||
authkey = config.get(CONF_AUTH_KEY)
|
||||
authproto: str = config[CONF_AUTH_PROTOCOL]
|
||||
privkey = config.get(CONF_PRIV_KEY)
|
||||
privproto: str = config[CONF_PRIV_PROTOCOL]
|
||||
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
payload_off = config.get(CONF_PAYLOAD_OFF)
|
||||
vartype = config.get(CONF_VARTYPE)
|
||||
payload_on: str = config[CONF_PAYLOAD_ON]
|
||||
payload_off: str = config[CONF_PAYLOAD_OFF]
|
||||
vartype: str = config[CONF_VARTYPE]
|
||||
|
||||
if version == "3":
|
||||
if not authkey:
|
||||
@@ -159,9 +166,11 @@ async def async_setup_platform(
|
||||
else:
|
||||
auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version])
|
||||
|
||||
transport = UdpTransportTarget((host, port))
|
||||
request_args = await async_create_request_cmd_args(
|
||||
hass, auth_data, UdpTransportTarget((host, port)), baseoid
|
||||
hass, auth_data, transport, baseoid
|
||||
)
|
||||
command_args = await async_create_command_cmd_args(hass, auth_data, transport)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
@@ -177,6 +186,7 @@ async def async_setup_platform(
|
||||
command_payload_off,
|
||||
vartype,
|
||||
request_args,
|
||||
command_args,
|
||||
)
|
||||
],
|
||||
True,
|
||||
@@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
host,
|
||||
port,
|
||||
baseoid,
|
||||
commandoid,
|
||||
payload_on,
|
||||
payload_off,
|
||||
command_payload_on,
|
||||
command_payload_off,
|
||||
vartype,
|
||||
request_args,
|
||||
name: str,
|
||||
host: str,
|
||||
port: int,
|
||||
baseoid: str,
|
||||
commandoid: str | None,
|
||||
payload_on: str,
|
||||
payload_off: str,
|
||||
command_payload_on: str | None,
|
||||
command_payload_off: str | None,
|
||||
vartype: str,
|
||||
request_args: RequestArgsType,
|
||||
command_args: CommandArgsType,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
|
||||
self._name = name
|
||||
self._attr_name = name
|
||||
self._baseoid = baseoid
|
||||
self._vartype = vartype
|
||||
|
||||
@@ -215,7 +226,8 @@ class SnmpSwitch(SwitchEntity):
|
||||
self._payload_on = payload_on
|
||||
self._payload_off = payload_off
|
||||
self._target = UdpTransportTarget((host, port))
|
||||
self._request_args: RequestArgsType = request_args
|
||||
self._request_args = request_args
|
||||
self._command_args = command_args
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
@@ -226,7 +238,7 @@ class SnmpSwitch(SwitchEntity):
|
||||
"""Turn off the switch."""
|
||||
await self._execute_command(self._command_payload_off)
|
||||
|
||||
async def _execute_command(self, command):
|
||||
async def _execute_command(self, command: str) -> None:
|
||||
# User did not set vartype and command is not a digit
|
||||
if self._vartype == "none" and not self._command_payload_on.isdigit():
|
||||
await self._set(command)
|
||||
@@ -265,14 +277,12 @@ class SnmpSwitch(SwitchEntity):
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the switch's name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if switch is on; False if off. None if unknown."""
|
||||
return self._state
|
||||
|
||||
async def _set(self, value):
|
||||
await setCmd(*self._request_args, value)
|
||||
async def _set(self, value: Any) -> None:
|
||||
"""Set the state of the switch."""
|
||||
await setCmd(
|
||||
*self._command_args, ObjectType(ObjectIdentity(self._commandoid), value)
|
||||
)
|
||||
|
||||
@@ -25,6 +25,14 @@ DATA_SNMP_ENGINE = "snmp_engine"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type CommandArgsType = tuple[
|
||||
SnmpEngine,
|
||||
UsmUserData | CommunityData,
|
||||
UdpTransportTarget | Udp6TransportTarget,
|
||||
ContextData,
|
||||
]
|
||||
|
||||
|
||||
type RequestArgsType = tuple[
|
||||
SnmpEngine,
|
||||
UsmUserData | CommunityData,
|
||||
@@ -34,20 +42,34 @@ type RequestArgsType = tuple[
|
||||
]
|
||||
|
||||
|
||||
async def async_create_command_cmd_args(
|
||||
hass: HomeAssistant,
|
||||
auth_data: UsmUserData | CommunityData,
|
||||
target: UdpTransportTarget | Udp6TransportTarget,
|
||||
) -> CommandArgsType:
|
||||
"""Create command arguments.
|
||||
|
||||
The ObjectType needs to be created dynamically by the caller.
|
||||
"""
|
||||
engine = await async_get_snmp_engine(hass)
|
||||
return (engine, auth_data, target, ContextData())
|
||||
|
||||
|
||||
async def async_create_request_cmd_args(
|
||||
hass: HomeAssistant,
|
||||
auth_data: UsmUserData | CommunityData,
|
||||
target: UdpTransportTarget | Udp6TransportTarget,
|
||||
object_id: str,
|
||||
) -> RequestArgsType:
|
||||
"""Create request arguments."""
|
||||
return (
|
||||
await async_get_snmp_engine(hass),
|
||||
auth_data,
|
||||
target,
|
||||
ContextData(),
|
||||
ObjectType(ObjectIdentity(object_id)),
|
||||
"""Create request arguments.
|
||||
|
||||
The same ObjectType is used for all requests.
|
||||
"""
|
||||
engine, auth_data, target, context_data = await async_create_command_cmd_args(
|
||||
hass, auth_data, target
|
||||
)
|
||||
object_type = ObjectType(ObjectIdentity(object_id))
|
||||
return (engine, auth_data, target, context_data, object_type)
|
||||
|
||||
|
||||
@singleton(DATA_SNMP_ENGINE)
|
||||
|
||||
@@ -62,6 +62,7 @@ class SpcAlarm(AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
| AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
)
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(self, area: Area, api: SpcWebGateway) -> None:
|
||||
"""Initialize the SPC alarm panel."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED
|
||||
from synology_dsm.exceptions import (
|
||||
SynologyDSMAPIErrorException,
|
||||
@@ -40,7 +41,7 @@ DEFAULT_PORT = 5000
|
||||
DEFAULT_PORT_SSL = 5001
|
||||
# Options
|
||||
DEFAULT_SCAN_INTERVAL = 15 # min
|
||||
DEFAULT_TIMEOUT = 30 # sec
|
||||
DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15)
|
||||
DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
|
||||
|
||||
ENTITY_UNIT_LOAD = "load"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.4.2"],
|
||||
"requirements": ["py-synologydsm-api==2.4.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
||||
@@ -37,7 +37,6 @@ from .const import (
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
CONST_OVERLAY_TIMER,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
HA_TERMINATION_DURATION,
|
||||
@@ -65,7 +64,7 @@ from .const import (
|
||||
TYPE_HEATING,
|
||||
)
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_overlay_mode
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -603,14 +602,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
overlay_mode=overlay_mode,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
|
||||
duration = (
|
||||
int(self._tado_zone_data.default_overlay_termination_duration)
|
||||
if self._tado_zone_data.default_overlay_termination_duration is not None
|
||||
else 3600
|
||||
)
|
||||
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Switching to %s for zone %s (%d) with temperature %s °C and duration"
|
||||
|
||||
@@ -212,3 +212,5 @@ SERVICE_ADD_METER_READING = "add_meter_reading"
|
||||
CONF_CONFIG_ENTRY = "config_entry"
|
||||
CONF_READING = "reading"
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback"
|
||||
|
||||
@@ -29,3 +29,23 @@ def decide_overlay_mode(
|
||||
)
|
||||
|
||||
return overlay_mode
|
||||
|
||||
|
||||
def decide_duration(
|
||||
tado: TadoConnector,
|
||||
duration: int | None,
|
||||
zone_id: int,
|
||||
overlay_mode: str | None = None,
|
||||
) -> None | int:
|
||||
"""Return correct duration based on the selected overlay mode/duration and tado config."""
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
|
||||
duration = (
|
||||
int(tado.data["zone"][zone_id].default_overlay_termination_duration)
|
||||
if tado.data["zone"][zone_id].default_overlay_termination_duration
|
||||
is not None
|
||||
else 3600
|
||||
)
|
||||
|
||||
return duration
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Repair implementations."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
DOMAIN,
|
||||
WATER_HEATER_FALLBACK_REPAIR,
|
||||
)
|
||||
|
||||
|
||||
def manage_water_heater_fallback_issue(
|
||||
hass: HomeAssistant,
|
||||
water_heater_entities: list,
|
||||
integration_overlay_fallback: str | None,
|
||||
) -> None:
|
||||
"""Notify users about water heater respecting fallback setting."""
|
||||
if (
|
||||
integration_overlay_fallback
|
||||
in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL]
|
||||
and len(water_heater_entities) > 0
|
||||
):
|
||||
for water_heater_entity in water_heater_entities:
|
||||
ir.async_create_issue(
|
||||
hass=hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=WATER_HEATER_FALLBACK_REPAIR,
|
||||
)
|
||||
@@ -165,6 +165,10 @@
|
||||
"import_failed_invalid_auth": {
|
||||
"title": "Failed to import, invalid credentials",
|
||||
"description": "Failed to import the configuration for the Tado Device Tracker, due to invalid credentials. Please fix the YAML configuration and restart Home Assistant. Alternatively you can use the UI to configure Tado. Don't forget to delete the YAML configuration, once the import is successful."
|
||||
},
|
||||
"water_heater_fallback": {
|
||||
"title": "Tado Water Heater entities now support fallback options",
|
||||
"description": "Due to added support for water heaters entities, these entities may use different overlay. Please configure integration entity and tado app water heater zone overlay options."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ from .const import (
|
||||
TYPE_HOT_WATER,
|
||||
)
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_overlay_mode
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
from .repairs import manage_water_heater_fallback_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,6 +81,12 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
manage_water_heater_fallback_issue(
|
||||
hass=hass,
|
||||
water_heater_entities=entities,
|
||||
integration_overlay_fallback=tado.fallback,
|
||||
)
|
||||
|
||||
|
||||
def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]:
|
||||
"""Create all water heater entities."""
|
||||
@@ -283,7 +290,12 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Switching to %s for zone %s (%d) with temperature %s",
|
||||
self._current_tado_hvac_mode,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["ttn_client==0.0.4"]
|
||||
"requirements": ["ttn_client==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,13 @@ class TibberNotificationService(BaseNotificationService):
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to Tibber devices."""
|
||||
migrate_notify_issue(self.hass, TIBBER_DOMAIN, "Tibber", "2024.12.0")
|
||||
migrate_notify_issue(
|
||||
self.hass,
|
||||
TIBBER_DOMAIN,
|
||||
"Tibber",
|
||||
"2024.12.0",
|
||||
service_name=self._service_name,
|
||||
)
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
try:
|
||||
await self._notify(title=title, message=message)
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity
|
||||
@@ -22,7 +21,7 @@ class ListAddItemIntent(intent.IntentHandler):
|
||||
|
||||
intent_type = INTENT_LIST_ADD_ITEM
|
||||
description = "Add item to a todo list"
|
||||
slot_schema = {"item": cv.string, "name": cv.string}
|
||||
slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string}
|
||||
platforms = {DOMAIN}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
@@ -37,18 +36,19 @@ class ListAddItemIntent(intent.IntentHandler):
|
||||
target_list: TodoListEntity | None = None
|
||||
|
||||
# Find matching list
|
||||
for list_state in intent.async_match_states(
|
||||
hass, name=list_name, domains=[DOMAIN]
|
||||
):
|
||||
target_list = component.get_entity(list_state.entity_id)
|
||||
if target_list is not None:
|
||||
break
|
||||
match_constraints = intent.MatchTargetsConstraints(
|
||||
name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant
|
||||
)
|
||||
match_result = intent.async_match_targets(hass, match_constraints)
|
||||
if not match_result.is_match:
|
||||
raise intent.MatchFailedError(
|
||||
result=match_result, constraints=match_constraints
|
||||
)
|
||||
|
||||
target_list = component.get_entity(match_result.states[0].entity_id)
|
||||
if target_list is None:
|
||||
raise intent.IntentHandleError(f"No to-do list: {list_name}")
|
||||
|
||||
assert target_list is not None
|
||||
|
||||
# Add to list
|
||||
await target_list.async_create_todo_item(
|
||||
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)
|
||||
|
||||
@@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
|
||||
"""Tuya Alarm Entity."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_code_arm_required = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user