mirror of
https://github.com/home-assistant/core.git
synced 2026-06-29 01:55:20 +02:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e62ff35fd | |||
| 0a5c1ef8eb | |||
| a88093afd2 | |||
| 9fd90283b3 | |||
| 3159242b68 | |||
| f5985b03e4 | |||
| 040a3bcb10 | |||
| 5aaf6704a9 | |||
| 2fcd00b301 | |||
| 0b439e6e4c | |||
| d13a5b7eec | |||
| de49716ec1 | |||
| 67c6921847 | |||
| 002b638013 | |||
| 4b60ed30c7 | |||
| 6f1deec507 | |||
| 227ba8032f | |||
| 7da3ecf033 | |||
| 8b293a18d3 | |||
| 3dc077f280 | |||
| d368a95323 | |||
| 495f41a742 | |||
| 9f7529706d | |||
| 8f6b1dff9c | |||
| f260a1bb7b | |||
| 157e137ea9 | |||
| b2e1a296d4 | |||
| e78a2c9f01 | |||
| 9011225a42 | |||
| 81ef9b99c2 | |||
| fa0207698a | |||
| 275883a95a | |||
| ebd252a225 | |||
| 2de6c0281d | |||
| f95671f0f4 | |||
| 5fcae9ecf7 | |||
| 0b86cfa496 | |||
| d45bdf37d5 | |||
| a9205df4a3 | |||
| c333744fd2 | |||
| 2f64601990 | |||
| cbd35be271 | |||
| 92ac14f42a | |||
| 45e568c73e | |||
| a121b8d146 | |||
| a2bd7d5857 | |||
| a6e639377b | |||
| 2147a851c3 | |||
| 9034afd29e | |||
| 5c5d259f63 | |||
| cc16a9086f | |||
| 5d1f8f770c | |||
| cea6b9b0b7 | |||
| 77f7c26399 | |||
| 8e0a5b258c | |||
| f8b942818c | |||
| 9660d12c77 | |||
| 7f1533a6e1 | |||
| 336d9e9126 | |||
| 1dde2d918e | |||
| 34a6b0ca61 | |||
| e92286ecd6 | |||
| 82bb9748db | |||
| 68e5e58a1c | |||
| f3e8403e9a | |||
| 28076bcad6 | |||
| ff25428e56 | |||
| 608acd422f | |||
| c860e83ec9 | |||
| c9f3f4a265 | |||
| e346a801d1 | |||
| a5c193931f | |||
| d273350db1 | |||
| 45f27b8b6e | |||
| d3208a420f | |||
| d0d35e380f | |||
| 2735e58d7f | |||
| ad3eab80c3 | |||
| 18e5d284b4 | |||
| e5052eaf44 | |||
| 62c2e8d2fd | |||
| 1f505067dd | |||
| 72875b3b5e | |||
| 3be755e496 | |||
| 5285798052 | |||
| da49e37946 | |||
| 2f9de98f2d | |||
| 383a6426fc | |||
| 5ed60cd057 | |||
| a1250b7bfb | |||
| 240e5219ad | |||
| 418f352ce7 | |||
| 599967b1d8 | |||
| ad82729357 |
@@ -37,6 +37,9 @@
|
||||
"title": "Re-authenticate AirVisual"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"type": "Integration type"
|
||||
},
|
||||
"description": "Pick what type of AirVisual data you want to monitor.",
|
||||
"title": "Configure AirVisual"
|
||||
}
|
||||
|
||||
@@ -190,8 +190,11 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
await self._async_remove_device_stale(stale_devices)
|
||||
self.previous_devices = current_devices
|
||||
|
||||
current_routines = {slugify(routine) for routine in self.api.routines}
|
||||
if stale_routines := self.previous_routines - current_routines:
|
||||
current_routines = {
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}"
|
||||
for routine in self.api.routines
|
||||
}
|
||||
if stale_routines := (self.previous_routines - current_routines):
|
||||
await self._async_remove_routine_stale(stale_routines)
|
||||
self.previous_routines = current_routines
|
||||
|
||||
@@ -225,17 +228,19 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Remove stale routine."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for routine in stale_routines:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine,
|
||||
)
|
||||
for routine_unique_id in stale_routines:
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
|
||||
routine_unique_id,
|
||||
)
|
||||
if entity_id:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine_unique_id.replace(
|
||||
f"{slugify(self.config_entry.unique_id)}-", ""
|
||||
),
|
||||
)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async def sync_history_state(self) -> None:
|
||||
|
||||
@@ -12,7 +12,18 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
TO_REDACT = {
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
"access_token",
|
||||
"adp_token",
|
||||
"device_private_key",
|
||||
"refresh_token",
|
||||
"store_authentication_cookie",
|
||||
"title",
|
||||
"website_cookies",
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DOMAIN as EVENT_DOMAIN,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import _LOGGER
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_remove_entity_from_virtual_group
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -31,6 +38,11 @@ async def async_setup_entry(
|
||||
"""Set up Alexa Devices events based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Remove voice event from virtual groups
|
||||
await async_remove_entity_from_virtual_group(
|
||||
hass, coordinator, EVENT_DOMAIN, "voice_event"
|
||||
)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
@@ -42,6 +54,7 @@ async def async_setup_entry(
|
||||
AlexaVoiceEvent(coordinator, serial_num, event_desc)
|
||||
for event_desc in EVENTS
|
||||
for serial_num in new_devices
|
||||
if coordinator.data[serial_num].device_family != SPEAKER_GROUP_FAMILY
|
||||
)
|
||||
|
||||
_check_device()
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.0.0"]
|
||||
"requirements": ["aioamazondevices==14.1.3"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry, alexa_api_call
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_remove_dnd_from_virtual_group, async_update_unique_id
|
||||
from .utils import async_remove_entity_from_virtual_group, async_update_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -58,7 +58,9 @@ async def async_setup_entry(
|
||||
new_key = "dnd"
|
||||
|
||||
# Remove old DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
|
||||
await async_remove_entity_from_virtual_group(
|
||||
hass, coordinator, SWITCH_DOMAIN, old_key
|
||||
)
|
||||
|
||||
# Replace unique id for DND switch
|
||||
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
|
||||
|
||||
@@ -8,7 +8,6 @@ from aioamazondevices.const.schedules import (
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
@@ -38,23 +37,22 @@ async def async_update_unique_id(
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
|
||||
async def async_remove_dnd_from_virtual_group(
|
||||
async def async_remove_entity_from_virtual_group(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
platform: str,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Remove entity DND from virtual group."""
|
||||
"""Remove entity from virtual group."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||
)
|
||||
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||
_LOGGER.debug("Removed entity '%s' from virtual group", entity_id)
|
||||
|
||||
|
||||
async def async_remove_unsupported_notification_sensors(
|
||||
|
||||
@@ -34,11 +34,13 @@ def generate_site_selector_name(site: Site) -> str:
|
||||
|
||||
|
||||
def filter_sites(sites: list[Site]) -> list[Site]:
|
||||
"""Deduplicates the list of sites."""
|
||||
"""Filter out closed sites and deduplicate the list of sites."""
|
||||
filtered: list[Site] = []
|
||||
filtered_nmi: set[str] = set()
|
||||
|
||||
for site in sorted(sites, key=lambda site: site.status):
|
||||
if site.status == SiteStatus.CLOSED:
|
||||
continue
|
||||
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
|
||||
filtered.append(site)
|
||||
filtered_nmi.add(site.nmi)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0"]
|
||||
"requirements": ["hassil==3.7.0"]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
"description": "The credentials for {username} need to be updated",
|
||||
"title": "Re-authenticate Blink"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
"""The Brands integration."""
|
||||
|
||||
from collections import deque
|
||||
from collections.abc import Container, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, override
|
||||
from typing import Any, Final
|
||||
|
||||
from aiohttp import ClientError, web
|
||||
from aiohttp import ClientError, hdrs, web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback, valid_domain
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -109,18 +108,23 @@ def _read_brand_file(brand_dir: Path, image: str) -> bytes | None:
|
||||
class _BrandsBaseView(HomeAssistantView):
|
||||
"""Base view for serving brand images."""
|
||||
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the view."""
|
||||
self._hass = hass
|
||||
self._cache_dir = Path(hass.config.cache_path(DOMAIN))
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
return self._hass.data[DOMAIN]
|
||||
def _authenticate(self, request: web.Request) -> None:
|
||||
"""Authenticate the request using Bearer token or query token."""
|
||||
access_tokens: deque[str] = self._hass.data[DOMAIN]
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED] or request.query.get("token") in access_tokens
|
||||
)
|
||||
if not authenticated:
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized
|
||||
raise web.HTTPForbidden
|
||||
|
||||
async def _serve_from_custom_integration(
|
||||
self,
|
||||
@@ -236,6 +240,8 @@ class BrandsIntegrationView(_BrandsBaseView):
|
||||
image: str,
|
||||
) -> web.Response:
|
||||
"""Handle GET request for an integration brand image."""
|
||||
self._authenticate(request)
|
||||
|
||||
if not valid_domain(domain) or image not in ALLOWED_IMAGES:
|
||||
return web.Response(status=HTTPStatus.NOT_FOUND)
|
||||
|
||||
@@ -268,6 +274,8 @@ class BrandsHardwareView(_BrandsBaseView):
|
||||
image: str,
|
||||
) -> web.Response:
|
||||
"""Handle GET request for a hardware brand image."""
|
||||
self._authenticate(request)
|
||||
|
||||
if not CATEGORY_RE.match(category):
|
||||
return web.Response(status=HTTPStatus.NOT_FOUND)
|
||||
# Hardware images have dynamic names like "manufacturer_model.png"
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.23.2"]
|
||||
"requirements": ["bthome-ble==3.23.4"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Awaitable, Callable, Container, Coroutine, Mapping
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from contextlib import suppress
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timedelta
|
||||
@@ -12,16 +12,16 @@ import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, final, override
|
||||
from typing import Any, Final, final
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import hdrs, web
|
||||
import attr
|
||||
from propcache.api import cached_property, under_cached_property
|
||||
import voluptuous as vol
|
||||
from webrtc_models import RTCIceCandidateInit
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
@@ -776,26 +776,30 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
class CameraView(HomeAssistantView):
|
||||
"""Base CameraView."""
|
||||
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, component: EntityComponent[Camera]) -> None:
|
||||
"""Initialize a basic camera view."""
|
||||
self.component = component
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
if (camera := self.component.get_entity(match_info["entity_id"])) is None:
|
||||
return ()
|
||||
|
||||
return camera.access_tokens
|
||||
|
||||
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
|
||||
"""Start a GET request."""
|
||||
if (camera := self.component.get_entity(entity_id)) is None:
|
||||
raise web.HTTPNotFound
|
||||
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED]
|
||||
or request.query.get("token") in camera.access_tokens
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
# Attempt with invalid bearer token, raise unauthorized
|
||||
# so ban middleware can handle it.
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized
|
||||
# Invalid sigAuth or camera access token
|
||||
raise web.HTTPForbidden
|
||||
|
||||
if not camera.is_on:
|
||||
_LOGGER.debug("Camera is off")
|
||||
raise web.HTTPServiceUnavailable
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.3"]
|
||||
"requirements": ["aiocomelit==2.0.7"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.6.1"]
|
||||
"requirements": ["hassil==3.7.0", "home-assistant-intents==2026.6.1"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.7",
|
||||
"aiodiscover==3.2.4",
|
||||
"cached-ipaddress==1.1.1"
|
||||
"aiodiscover==3.3.2",
|
||||
"cached-ipaddress==1.1.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.7.0"]
|
||||
"requirements": ["dsmr-parser==1.9.0"]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "Please enter the API key obtained from ecobee.com."
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ class EconetFanModeSelect(EcoNetEntity[Thermostat], SelectEntity):
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return available select options."""
|
||||
return [e.value for e in self._econet.fan_modes]
|
||||
return [e.name for e in self._econet.fan_modes]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return current select option."""
|
||||
return self._econet.fan_mode.value
|
||||
return self._econet.fan_mode.name
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Set the selected option."""
|
||||
|
||||
@@ -246,8 +246,8 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
if device is not None and device.mac_address:
|
||||
await self.async_set_unique_id(dr.format_mac(device.mac_address))
|
||||
# aborts if user tried to switch devices
|
||||
self._abort_if_unique_id_mismatch()
|
||||
if reconfigure_entry.unique_id is not None:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
else:
|
||||
# If we cannot confirm identity, keep existing
|
||||
# behavior (don't block reconfigure)
|
||||
@@ -255,6 +255,7 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
unique_id=self.unique_id,
|
||||
data_updates={
|
||||
**reconfigure_entry.data,
|
||||
CONF_HOST: info[CONF_HOST],
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to set up {model} {id} ({ipaddr})?"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260527.5"]
|
||||
"requirements": ["home-assistant-frontend==20260527.7"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::device%]",
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
|
||||
@@ -189,7 +189,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
) from err
|
||||
total_info["todayEnergy"] = total_info["today_energy"]
|
||||
total_info["totalEnergy"] = total_info["total_energy"]
|
||||
total_info["invTodayPpv"] = total_info["current_power"]
|
||||
# V1 API returns current_power in kW, convert to W
|
||||
total_info["invTodayPpv"] = total_info["current_power"] * 1000
|
||||
else:
|
||||
# Classic API: use plant_info as before.
|
||||
# Copy the response to avoid mutating the dict returned by the library
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"title": "[%key:component::here_travel_time::config::step::destination_menu::title%]"
|
||||
},
|
||||
"destination_entity_id": {
|
||||
"destination_entity": {
|
||||
"data": {
|
||||
"destination_entity_id": "Destination using an entity"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"title": "[%key:component::here_travel_time::config::step::origin_menu::title%]"
|
||||
},
|
||||
"origin_entity_id": {
|
||||
"origin_entity": {
|
||||
"data": {
|
||||
"origin_entity_id": "Origin using an entity"
|
||||
},
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "The Honeywell integration needs to re-authenticate your account",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
|
||||
@@ -9,10 +9,12 @@ import time
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
import warnings
|
||||
|
||||
from aiohttp import ClientError, ClientResponse, ClientSession, web
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
import jwt
|
||||
from jwt.warnings import InsecureKeyLengthWarning
|
||||
from py_vapid import Vapid
|
||||
from pywebpush import WebPusher, WebPushException, webpush_async
|
||||
import voluptuous as vol
|
||||
@@ -325,7 +327,8 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||
if target_check.get(ATTR_TARGET) in self.registrations:
|
||||
possible_target = self.registrations[target_check[ATTR_TARGET]]
|
||||
key = possible_target["subscription"]["keys"]["auth"]
|
||||
with suppress(jwt.exceptions.DecodeError):
|
||||
with suppress(jwt.exceptions.DecodeError), warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
|
||||
return jwt.decode(token, key, algorithms=["ES256", "HS256"])
|
||||
|
||||
return self.json_message(
|
||||
@@ -585,7 +588,9 @@ def add_jwt(timestamp: int, target: str, tag: str, jwt_secret: str) -> str:
|
||||
ATTR_TARGET: target,
|
||||
ATTR_TAG: tag,
|
||||
}
|
||||
return jwt.encode(jwt_claims, jwt_secret)
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", InsecureKeyLengthWarning)
|
||||
return jwt.encode(jwt_claims, jwt_secret)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"id": "Hue bridge"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hue bridge."
|
||||
|
||||
@@ -85,6 +85,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
|
||||
entity_description = LightEntityDescription(
|
||||
key="hue_grouped_light",
|
||||
translation_key="hue_grouped_light",
|
||||
has_entity_name=True,
|
||||
name=None,
|
||||
)
|
||||
|
||||
@@ -166,8 +166,10 @@ class HueLightLevelSensor(HueSensorBase):
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the value reported by the sensor."""
|
||||
if self.resource.light.value is None:
|
||||
return None
|
||||
# Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm
|
||||
# scale used because the human eye adjusts to light levels and small
|
||||
# changes at low lux levels are more noticeable than at high lux
|
||||
|
||||
@@ -59,7 +59,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
|
||||
await hass.async_add_executor_job(account.setup)
|
||||
|
||||
entry.runtime_data = account
|
||||
entry.async_on_unload(account.cancel_fetch)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -68,4 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await hass.async_add_executor_job(entry.runtime_data.cancel_fetch)
|
||||
return unload_ok
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Container, Mapping
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
from typing import Final, final, override
|
||||
from typing import Final, final
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp import hdrs, web
|
||||
import httpx
|
||||
from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, KEY_HASS, HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONTENT_TYPE_MULTIPART, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import (
|
||||
@@ -315,28 +314,33 @@ class ImageView(HomeAssistantView):
|
||||
"""View to serve an image."""
|
||||
|
||||
name = "api:image:image"
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
url = "/api/image_proxy/{entity_id}"
|
||||
|
||||
def __init__(self, component: EntityComponent[ImageEntity]) -> None:
|
||||
"""Initialize an image view."""
|
||||
self.component = component
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
if (image_entity := self.component.get_entity(match_info["entity_id"])) is None:
|
||||
return ()
|
||||
|
||||
return image_entity.access_tokens
|
||||
|
||||
@callback
|
||||
def _get_image_entity(self, entity_id: str) -> ImageEntity:
|
||||
"""Get image entity from request."""
|
||||
async def _authenticate_request(
|
||||
self, request: web.Request, entity_id: str
|
||||
) -> ImageEntity:
|
||||
"""Authenticate request and return image entity."""
|
||||
if (image_entity := self.component.get_entity(entity_id)) is None:
|
||||
raise web.HTTPNotFound
|
||||
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED]
|
||||
or request.query.get("token") in image_entity.access_tokens
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
# Attempt with invalid bearer token, raise unauthorized
|
||||
# so ban middleware can handle it.
|
||||
if hdrs.AUTHORIZATION in request.headers:
|
||||
raise web.HTTPUnauthorized
|
||||
# Invalid sigAuth or image entity access token
|
||||
raise web.HTTPForbidden
|
||||
|
||||
return image_entity
|
||||
|
||||
async def head(self, request: web.Request, entity_id: str) -> web.Response:
|
||||
@@ -345,7 +349,7 @@ class ImageView(HomeAssistantView):
|
||||
This is sent by some DLNA renderers, like Samsung ones, prior to sending
|
||||
the GET request.
|
||||
"""
|
||||
image_entity = self._get_image_entity(entity_id)
|
||||
image_entity = await self._authenticate_request(request, entity_id)
|
||||
|
||||
# Don't use `handle` as we don't care about the stream case, we only want
|
||||
# to verify that the image exists.
|
||||
@@ -361,7 +365,7 @@ class ImageView(HomeAssistantView):
|
||||
|
||||
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
|
||||
"""Start a GET request."""
|
||||
image_entity = self._get_image_entity(entity_id)
|
||||
image_entity = await self._authenticate_request(request, entity_id)
|
||||
return await self.handle(request, image_entity)
|
||||
|
||||
async def handle(
|
||||
|
||||
@@ -248,7 +248,10 @@ def _generate_thumbnail_if_file_does_not_exist(
|
||||
if not target_file.is_file():
|
||||
image = ImageOps.exif_transpose(Image.open(original_path))
|
||||
image.thumbnail(target_size)
|
||||
image.save(target_path, format=content_type.partition("/")[-1])
|
||||
save_format = content_type.partition("/")[-1]
|
||||
if save_format == "jpeg" and image.mode not in ("RGB", "L", "CMYK"):
|
||||
image = image.convert("RGB")
|
||||
image.save(target_path, format=save_format)
|
||||
|
||||
|
||||
def _validate_size_from_filename(filename: str) -> tuple[int, int]:
|
||||
|
||||
@@ -81,6 +81,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Handle setup of the coordinator."""
|
||||
try:
|
||||
await self.api.async_setup()
|
||||
user_info = await self.api.users.async_get_my_user()
|
||||
except ImmichUnauthorizedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -119,7 +120,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
return ImmichData(
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioimmich==0.14.1"]
|
||||
"requirements": ["aioimmich==0.15.0"]
|
||||
}
|
||||
|
||||
@@ -225,10 +225,9 @@ class ImmichMediaSource(MediaSource):
|
||||
entry.title,
|
||||
)
|
||||
try:
|
||||
album_info = await immich_api.albums.async_get_album_info(
|
||||
identifier.collection_id
|
||||
assets = await immich_api.search.async_get_all_by_album_ids(
|
||||
[identifier.collection_id]
|
||||
)
|
||||
assets = album_info.assets
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -53,7 +53,7 @@ async def _async_upload_file(service_call: ServiceCall) -> None:
|
||||
|
||||
if target_album := service_call.data.get(CONF_ALBUM_ID):
|
||||
try:
|
||||
await coordinator.api.albums.async_get_album_info(target_album, True)
|
||||
await coordinator.api.albums.async_get_album_info(target_album)
|
||||
except ImmichError as ex:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -423,7 +423,7 @@ def get_influx_connection( # noqa: C901
|
||||
if CONF_HOST in conf:
|
||||
kwargs[CONF_HOST] = conf[CONF_HOST]
|
||||
|
||||
if (path := conf.get(CONF_PATH)) is not None:
|
||||
if (path := conf.get(CONF_PATH)) is not None and path != "/":
|
||||
kwargs[CONF_PATH] = path
|
||||
|
||||
if (port := conf.get(CONF_PORT)) is not None:
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"protocol": "Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your Iskra device."
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "Do you want to set up Islamic Prayer Times?",
|
||||
"title": "Set up Islamic Prayer Times"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyituran==0.1.5"]
|
||||
"requirements": ["pyituran==0.1.6"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"location": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -121,7 +121,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
@@ -163,7 +163,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
@@ -207,7 +207,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for LG TV running on NetCast 3 or 4."""
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -133,13 +134,22 @@ class LgTVDevice(MediaPlayerEntity):
|
||||
|
||||
channel_list = client.query_data("channel_list")
|
||||
if channel_list:
|
||||
channel_names = []
|
||||
channel_pairs = []
|
||||
for channel in channel_list:
|
||||
channel_name = channel.find("chname")
|
||||
if channel_name is not None:
|
||||
channel_names.append(str(channel_name.text))
|
||||
self._sources = dict(zip(channel_names, channel_list, strict=False))
|
||||
# sort source names by the major channel number
|
||||
channel_pairs.append((str(channel_name.text), channel))
|
||||
|
||||
name_count = Counter(name for name, _ in channel_pairs)
|
||||
|
||||
self._sources = {}
|
||||
for name, channel in channel_pairs:
|
||||
if name_count[name] > 1:
|
||||
major = channel.find("major")
|
||||
if major is not None:
|
||||
name = f"{name} ({major.text})"
|
||||
self._sources[name] = channel
|
||||
|
||||
source_tuples = [
|
||||
(k, source.find("major").text)
|
||||
for k, source in self._sources.items()
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
|
||||
},
|
||||
"step": {
|
||||
"import": {
|
||||
"import_ics_file": {
|
||||
"data": {
|
||||
"ics_file": "ICS file"
|
||||
},
|
||||
"description": "You can import events in iCal format (.ics file)."
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
from collections.abc import Callable, Container, Mapping
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import datetime as dt
|
||||
from enum import StrEnum
|
||||
@@ -12,7 +12,7 @@ import hashlib
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import secrets
|
||||
from typing import Any, Final, Required, TypedDict, final, override
|
||||
from typing import Any, Final, Required, TypedDict, final
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
import aiohttp
|
||||
@@ -24,7 +24,7 @@ import voluptuous as vol
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ( # noqa: F401
|
||||
@@ -50,7 +50,7 @@ from homeassistant.const import ( # noqa: F401
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, SupportsResponse, callback
|
||||
from homeassistant.core import HomeAssistant, SupportsResponse
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
@@ -1248,7 +1248,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
class MediaPlayerImageView(HomeAssistantView):
|
||||
"""Media player view to serve an image."""
|
||||
|
||||
use_query_token_for_auth = True
|
||||
requires_auth = False
|
||||
url = "/api/media_player_proxy/{entity_id}"
|
||||
name = "api:media_player:image"
|
||||
extra_urls = [
|
||||
@@ -1261,15 +1261,6 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||
"""Initialize a media player view."""
|
||||
self.component = component
|
||||
|
||||
@callback
|
||||
@override
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
if (player := self.component.get_entity(match_info["entity_id"])) is None:
|
||||
return ()
|
||||
|
||||
return (player.access_token,)
|
||||
|
||||
async def get(
|
||||
self,
|
||||
request: web.Request,
|
||||
@@ -1279,9 +1270,21 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||
) -> web.Response:
|
||||
"""Start a get request."""
|
||||
if (player := self.component.get_entity(entity_id)) is None:
|
||||
return web.Response(status=HTTPStatus.NOT_FOUND)
|
||||
status = (
|
||||
HTTPStatus.NOT_FOUND
|
||||
if request[KEY_AUTHENTICATED]
|
||||
else HTTPStatus.UNAUTHORIZED
|
||||
)
|
||||
return web.Response(status=status)
|
||||
|
||||
assert isinstance(player, MediaPlayerEntity)
|
||||
authenticated = (
|
||||
request[KEY_AUTHENTICATED]
|
||||
or request.query.get("token") == player.access_token
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
return web.Response(status=HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
if media_content_type and media_content_id:
|
||||
media_image_id = request.query.get("media_image_id")
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
|
||||
"title": "Discovered Melnor Bluetooth valve"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"code": "Station code"
|
||||
"station_code": "Station code"
|
||||
},
|
||||
"data_description": {
|
||||
"code": "Looks like ESCAT4300000043206B"
|
||||
"station_code": "Looks like ESCAT4300000043206B"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,19 +508,20 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
|
||||
solar_save = 9, 34
|
||||
gentle = 10, 35, 210
|
||||
extra_quiet = 11, 36, 207
|
||||
hygiene = 12, 37
|
||||
quick_power_wash = 13, 38
|
||||
hygiene = 12, 37, 206
|
||||
quick_power_wash = 13, 38, 216
|
||||
pasta_paela = 14
|
||||
tall_items = 17, 42
|
||||
glasses_warm = 19
|
||||
quick_intense = 21
|
||||
normal = 23, 30
|
||||
normal = 23, 30, 217
|
||||
pre_wash = 24
|
||||
pot_rests_and_filters = 25
|
||||
power_wash = 44, 204
|
||||
comfort_wash = 203
|
||||
comfort_wash_plus = 209
|
||||
rinse_salt = 215
|
||||
rinse_and_hold = 219
|
||||
|
||||
|
||||
class TumbleDryerProgramId(MieleEnum, missing_to_none=True):
|
||||
|
||||
@@ -135,6 +135,8 @@ class MieleFan(MieleEntity, FanEntity):
|
||||
_LOGGER.debug("Calc ventilation_step: %s", ventilation_step)
|
||||
if ventilation_step == 0:
|
||||
await self.async_turn_off()
|
||||
elif ventilation_step == self.device.state_ventilation_step:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
await self.api.send_action(
|
||||
|
||||
@@ -791,6 +791,7 @@
|
||||
"rice_pudding_rapid_steam_cooking": "Rice pudding (rapid steam cooking)",
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_and_hold": "Rinse and hold",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
|
||||
@@ -42,12 +42,23 @@ class MitsubishiComfortCoordinator(DataUpdateCoordinator[IndoorUnit | KumoStatio
|
||||
try:
|
||||
success = await self.device.update_status()
|
||||
except Exception as err:
|
||||
# The user-facing UpdateFailed message is translated and omits the IP;
|
||||
# log it here so the failing address is visible in debug logs.
|
||||
_LOGGER.debug(
|
||||
"Error polling %s at %s: %s",
|
||||
self.device.name,
|
||||
self.device.address,
|
||||
err,
|
||||
)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"device_name": self.device.name},
|
||||
) from err
|
||||
if not success:
|
||||
_LOGGER.debug(
|
||||
"%s at %s returned no data", self.device.name, self.device.address
|
||||
)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
"device": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call."
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"blind_type": "Blind type"
|
||||
},
|
||||
"description": "What kind of blind is {display_name}?"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -4179,7 +4179,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
}
|
||||
except AddonError:
|
||||
# We do not have discovery information yet
|
||||
@@ -4420,7 +4419,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: data.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: data.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1432,9 +1432,10 @@ class MqttEntity(
|
||||
if (
|
||||
self._config[CONF_ENABLED_BY_DEFAULT]
|
||||
and deleted_entry
|
||||
and deleted_entry.disabled_by is not None
|
||||
and deleted_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION
|
||||
):
|
||||
# Enable previous deleted entity and enable it
|
||||
# Enable previous deleted entity and enable it,
|
||||
# if it was not disabled by the user
|
||||
recreated_entry = entity_registry.async_get_or_create(
|
||||
entity_platform, DOMAIN, self.unique_id
|
||||
)
|
||||
|
||||
@@ -52,5 +52,5 @@ class OpenGarageEntity(CoordinatorEntity[OpenGarageDataUpdateCoordinator]):
|
||||
manufacturer="Open Garage",
|
||||
name=self.coordinator.data["name"],
|
||||
suggested_area="Garage",
|
||||
sw_version=self.coordinator.data["fwv"],
|
||||
sw_version=str(self.coordinator.data["fwv"]),
|
||||
)
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["opower==0.18.4"]
|
||||
"requirements": ["opower==0.18.5"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"code": "Verification code (OTP)"
|
||||
"code": "Verification code (OTP)",
|
||||
"qr_code": "QR code"
|
||||
},
|
||||
"data_description": {
|
||||
"code": "The six-digit code currently displayed in your authentication app."
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"update_interval": "Update interval (minutes)"
|
||||
"scan_interval": "Update interval (minutes)"
|
||||
},
|
||||
"description": "Set the update interval (minutes)",
|
||||
"title": "Options for Plaato"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyrainbird"],
|
||||
"requirements": ["pyrainbird==6.3.0"]
|
||||
"requirements": ["pyrainbird==6.3.1"]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class RainMachineEntity(CoordinatorEntity[RainMachineDataUpdateCoordinator]):
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)},
|
||||
name=self._data.controller.name.capitalize(),
|
||||
manufacturer="RainMachine",
|
||||
hw_version=self._version_coordinator.data["hwVer"],
|
||||
hw_version=str(self._version_coordinator.data["hwVer"]),
|
||||
sw_version=f"{self._version_coordinator.data['swVer']} "
|
||||
f"(API: {self._version_coordinator.data['apiVer']})",
|
||||
)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.20.1"]
|
||||
"requirements": ["reolink-aio==0.21.0"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Base class for Rituals Perfume Genie diffuser entity."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -12,6 +14,12 @@ MODEL = "The Perfume Genie"
|
||||
MODEL2 = "The Perfume Genie 2.0"
|
||||
|
||||
|
||||
def _version_string(version: Any) -> str:
|
||||
if isinstance(version, dict):
|
||||
return str(version.get("title", version))
|
||||
return str(version)
|
||||
|
||||
|
||||
class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]):
|
||||
"""Representation of a diffuser entity."""
|
||||
|
||||
@@ -31,7 +39,7 @@ class DiffuserEntity(CoordinatorEntity[RitualsDataUpdateCoordinator]):
|
||||
manufacturer=MANUFACTURER,
|
||||
model=MODEL if coordinator.diffuser.has_battery else MODEL2,
|
||||
name=coordinator.diffuser.name,
|
||||
sw_version=coordinator.diffuser.version,
|
||||
sw_version=_version_string(coordinator.diffuser.version),
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -40,6 +40,7 @@ ENTITY_DESCRIPTIONS = (
|
||||
key="fill",
|
||||
translation_key="fill",
|
||||
value_fn=lambda diffuser: diffuser.fill,
|
||||
has_fn=lambda diffuser: "fillc" in diffuser.hub_data.get("sensors", {}),
|
||||
),
|
||||
RitualsSensorEntityDescription(
|
||||
key="perfume",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/scrape",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"]
|
||||
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.1.1"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,38 @@
|
||||
"user": "Add sensor"
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"index": "[%key:component::scrape::config_subentries::entity::step::user::data::index%]",
|
||||
"select": "[%key:component::scrape::config_subentries::entity::step::user::data::select%]"
|
||||
},
|
||||
"data_description": {
|
||||
"index": "[%key:component::scrape::config_subentries::entity::step::user::data_description::index%]",
|
||||
"select": "[%key:component::scrape::config_subentries::entity::step::user::data_description::select%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"data": {
|
||||
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::attribute%]",
|
||||
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::availability%]",
|
||||
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::device_class%]",
|
||||
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::value_template%]"
|
||||
},
|
||||
"data_description": {
|
||||
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::attribute%]",
|
||||
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::availability%]",
|
||||
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::device_class%]",
|
||||
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::value_template%]"
|
||||
},
|
||||
"description": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::description%]",
|
||||
"name": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"index": "Index",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"additional_account": {
|
||||
"add_account": {
|
||||
"data": {
|
||||
"account": "[%key:component::sia::config::step::user::data::account%]",
|
||||
"additional_account": "[%key:component::sia::config::step::user::data::additional_account%]",
|
||||
|
||||
@@ -247,7 +247,7 @@ def _async_register_base_station(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, str(system.system_id))},
|
||||
manufacturer="SimpliSafe",
|
||||
model=system.version,
|
||||
model=str(system.version),
|
||||
name=system.address,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
"address": "[%key:common::config_flow::data::device%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]"
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ from .const import (
|
||||
MODELS_TV_ONLY,
|
||||
PLAYABLE_MEDIA_TYPES,
|
||||
SONOS_CREATE_MEDIA_PLAYER,
|
||||
SONOS_FAVORITES_UPDATED,
|
||||
SONOS_MEDIA_UPDATED,
|
||||
SONOS_STATE_PLAYING,
|
||||
SONOS_STATE_TRANSITIONING,
|
||||
@@ -131,7 +132,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -145,6 +145,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
super().__init__(speaker, config_entry)
|
||||
self._attr_unique_id = self.soco.uid
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
features = self._attr_supported_features
|
||||
if self.source_list:
|
||||
features |= MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
return features
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle common setup when added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -155,6 +163,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
self.async_write_media_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SONOS_FAVORITES_UPDATED}-{self.speaker.household_id}",
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_write_media_state(self, uid: str) -> None:
|
||||
@@ -394,14 +409,18 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
sources: list[str] = []
|
||||
model = self.coordinator.model_name.split()[-1].upper()
|
||||
if model in MODELS_LINEIN_ONLY:
|
||||
return [SOURCE_LINEIN]
|
||||
if model in MODELS_TV_ONLY:
|
||||
return [SOURCE_TV]
|
||||
if model in MODELS_LINEIN_AND_TV:
|
||||
return [SOURCE_LINEIN, SOURCE_TV]
|
||||
return []
|
||||
sources = [SOURCE_LINEIN]
|
||||
elif model in MODELS_TV_ONLY:
|
||||
sources = [SOURCE_TV]
|
||||
elif model in MODELS_LINEIN_AND_TV:
|
||||
sources = [SOURCE_LINEIN, SOURCE_TV]
|
||||
sources.extend(
|
||||
fav.title for fav in self.speaker.favorites if fav.title not in sources
|
||||
)
|
||||
return sources
|
||||
|
||||
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
||||
def media_play(self) -> None:
|
||||
|
||||
@@ -160,7 +160,7 @@ async def async_setup_entry(
|
||||
model=model,
|
||||
manufacturer=manufacturer,
|
||||
model_id=model_id,
|
||||
hw_version=player.firmware,
|
||||
hw_version=str(player.firmware) if player.firmware is not None else None,
|
||||
sw_version=sw_version,
|
||||
via_device=(DOMAIN, coordinator.server_uuid),
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ VEHICLE_STATUS = "vehicle_status"
|
||||
API_GEN_1 = "g1"
|
||||
API_GEN_2 = "g2"
|
||||
API_GEN_3 = "g3"
|
||||
API_GEN_4 = "g4"
|
||||
MANUFACTURER = "Subaru"
|
||||
|
||||
PLATFORMS = [
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import get_device_info
|
||||
from .const import (
|
||||
API_GEN_2,
|
||||
API_GEN_3,
|
||||
API_GEN_4,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_STATUS,
|
||||
@@ -153,10 +154,10 @@ def create_vehicle_sensors(
|
||||
sensor_descriptions_to_add = []
|
||||
sensor_descriptions_to_add.extend(SAFETY_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3]:
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3, API_GEN_4]:
|
||||
sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_API_GEN] == API_GEN_3:
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_3, API_GEN_4]:
|
||||
sensor_descriptions_to_add.extend(API_GEN_3_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_HAS_EV]:
|
||||
|
||||
@@ -36,8 +36,11 @@ class SwitchBotCloudEntity(CoordinatorEntity[SwitchBotCoordinator]):
|
||||
self._api = api
|
||||
self._attr_unique_id = device.device_id
|
||||
_sw_version = None
|
||||
if self.coordinator.data is not None:
|
||||
_sw_version = self.coordinator.data.get("version")
|
||||
if (
|
||||
self.coordinator.data is not None
|
||||
and (_version := self.coordinator.data.get("version")) is not None
|
||||
):
|
||||
_sw_version = str(_version)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.device_id)},
|
||||
name=device.device_name,
|
||||
|
||||
@@ -61,6 +61,7 @@ def setup_platform(
|
||||
if http_error.response.status_code == requests.codes.unauthorized:
|
||||
_LOGGER.error("Invalid credentials")
|
||||
return
|
||||
raise
|
||||
|
||||
all_sensors = []
|
||||
for device in devices:
|
||||
|
||||
@@ -250,13 +250,9 @@ class DatasetStore:
|
||||
entry: DatasetEntry | None
|
||||
for entry in self.datasets.values():
|
||||
if entry.dataset == dataset:
|
||||
if (
|
||||
preferred_extended_address
|
||||
and entry.preferred_extended_address is None
|
||||
):
|
||||
self.async_set_preferred_border_agent(
|
||||
entry.id, preferred_border_agent_id, preferred_extended_address
|
||||
)
|
||||
self._async_maybe_update_preferred_border_agent(
|
||||
entry, preferred_border_agent_id, preferred_extended_address
|
||||
)
|
||||
return
|
||||
|
||||
# Update if dataset with same extended pan id exists and the timestamp
|
||||
@@ -307,10 +303,9 @@ class DatasetStore:
|
||||
self.datasets[entry.id], tlv=tlv
|
||||
)
|
||||
self.async_schedule_save()
|
||||
if preferred_extended_address and entry.preferred_extended_address is None:
|
||||
self.async_set_preferred_border_agent(
|
||||
entry.id, preferred_border_agent_id, preferred_extended_address
|
||||
)
|
||||
self._async_maybe_update_preferred_border_agent(
|
||||
entry, preferred_border_agent_id, preferred_extended_address
|
||||
)
|
||||
return
|
||||
|
||||
entry = DatasetEntry(
|
||||
@@ -348,6 +343,37 @@ class DatasetStore:
|
||||
"""Get dataset by id."""
|
||||
return self.datasets.get(dataset_id)
|
||||
|
||||
@callback
|
||||
def _async_maybe_update_preferred_border_agent(
|
||||
self,
|
||||
entry: DatasetEntry,
|
||||
preferred_border_agent_id: str | None,
|
||||
preferred_extended_address: str | None,
|
||||
) -> None:
|
||||
"""Update the preferred border agent of an existing dataset if appropriate.
|
||||
|
||||
Sets the preferred border agent if it was not set yet, or refreshes the
|
||||
stored extended address when the border agent ID still matches but the
|
||||
extended address changed. The latter happens e.g. after an OTBR upgrade
|
||||
regenerates the extended address while keeping the same border agent ID.
|
||||
"""
|
||||
if not preferred_extended_address:
|
||||
return
|
||||
if entry.preferred_extended_address is None or (
|
||||
preferred_border_agent_id is not None
|
||||
and preferred_border_agent_id == entry.preferred_border_agent_id
|
||||
and preferred_extended_address != entry.preferred_extended_address
|
||||
):
|
||||
_LOGGER.info(
|
||||
"Updating extended address of preferred border agent %s from %s to %s",
|
||||
preferred_border_agent_id,
|
||||
entry.preferred_extended_address,
|
||||
preferred_extended_address,
|
||||
)
|
||||
self.async_set_preferred_border_agent(
|
||||
entry.id, preferred_border_agent_id, preferred_extended_address
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_set_preferred_border_agent(
|
||||
self, dataset_id: str, border_agent_id: str | None, extended_address: str
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.9.6"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"description": "The Tuya integration now uses an improved login method. To reauthenticate with your Smart Life or Tuya Smart account, you need to enter your user code.\n\nYou can find this code in the Smart Life app or Tuya Smart app in **Settings** > **Account and Security** screen, and enter the code shown on the **User Code** field. The user code is case-sensitive, please be sure to enter it exactly as shown in the app."
|
||||
},
|
||||
"scan": {
|
||||
"data": {
|
||||
"QR": "QR code"
|
||||
},
|
||||
"description": "Use the Smart Life app or Tuya Smart app to scan the following QR code to complete the login.\n\nContinue to the next step once you have completed this step in the app."
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -440,7 +440,7 @@ class ProtectSettableKeysMixin(ProtectEntityDescription[T]):
|
||||
|
||||
async def ufp_set(self, obj: T, value: Any) -> None:
|
||||
"""Set value for UniFi Protect device."""
|
||||
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.display_name)
|
||||
_LOGGER.debug("Setting %s to %s for %s", self.key, value, obj.display_name)
|
||||
if self.ufp_set_method is not None:
|
||||
await getattr(obj, self.ufp_set_method)(value)
|
||||
elif self.ufp_set_method_fn is not None:
|
||||
|
||||
@@ -8,8 +8,10 @@ from homeassistant.const import Platform
|
||||
|
||||
LOGGER: Logger = getLogger(__package__)
|
||||
|
||||
# The free plan is limited to 10 requests/minute
|
||||
COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=10)
|
||||
# The free plan is formally limited to 10 requests/minute
|
||||
# But real world says 5 requests/minute is the real limit
|
||||
# Opened a ticket with support with no response for 2 months
|
||||
COORDINATOR_UPDATE_INTERVAL: timedelta = timedelta(seconds=15)
|
||||
|
||||
DOMAIN: Final = "uptimerobot"
|
||||
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/v2c",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pytrydan==1.0.1"]
|
||||
"requirements": ["pytrydan==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import logging
|
||||
|
||||
from aiowebdav2.client import Client
|
||||
from aiowebdav2.exceptions import UnauthorizedError
|
||||
from aiowebdav2.exceptions import (
|
||||
ConnectionExceptionError,
|
||||
NoConnectionError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
|
||||
@@ -35,6 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_username_password",
|
||||
) from err
|
||||
except (ConnectionExceptionError, NoConnectionError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
# Check if we can connect to the WebDAV server
|
||||
# and access the root directory
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from weheat.abstractions.user import async_get_user_id_from_token
|
||||
from weheat.exceptions import ApiException
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
@@ -33,12 +34,17 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Override create entry to find heat pumps."""
|
||||
# get the user id and use that as unique id for this entry
|
||||
user_id = await async_get_user_id_from_token(
|
||||
API_URL,
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
user_id = await async_get_user_id_from_token(
|
||||
API_URL,
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
except ApiException as err:
|
||||
self.logger.error("Failed to get user ID from Weheat API: %s", err)
|
||||
return self.async_abort(reason="oauth_failed")
|
||||
if user_id is None:
|
||||
return self.async_abort(reason="oauth_failed")
|
||||
await self.async_set_unique_id(user_id)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
@@ -177,6 +177,6 @@ class XiaomiGatewayDevice(CoordinatorEntity[GatewayDeviceCoordinator], Entity):
|
||||
manufacturer="Xiaomi",
|
||||
name=self._sub_device.name,
|
||||
model=self._sub_device.model,
|
||||
sw_version=self._sub_device.firmware_version,
|
||||
sw_version=str(self._sub_device.firmware_version),
|
||||
hw_version=self._sub_device.zigbee_model,
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> boo
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
LOGGER.debug("Migrating from version %s", entry.version)
|
||||
LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
|
||||
|
||||
if entry.version == 1:
|
||||
new_options = entry.options.copy()
|
||||
@@ -55,6 +55,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bo
|
||||
del new_data[CONF_NAME]
|
||||
hass.config_entries.async_update_entry(entry, data=new_data, minor_version=2)
|
||||
|
||||
LOGGER.debug("Migration to version %s successful", entry.version)
|
||||
if entry.version == 2 and entry.minor_version == 2:
|
||||
entity_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_config_entry(entity_reg, entry.entry_id)
|
||||
for entity in entries:
|
||||
if entity.unique_id == "yale_smart_alarm-panic":
|
||||
entity_reg.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=f"{entry.entry_id}-panic",
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to version %s.%s successful", entry.version, entry.minor_version
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -47,7 +47,7 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity):
|
||||
"""Initialize the plug switch."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"yale_smart_alarm-{description.key}"
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
|
||||
@@ -64,7 +64,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Yale integration."""
|
||||
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 2
|
||||
MINOR_VERSION = 3
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
|
||||
@@ -432,15 +432,6 @@ async def async_set_credential(
|
||||
translation_key="no_available_credential_slots",
|
||||
translation_placeholders={"credential_type": cred_type_str},
|
||||
)
|
||||
elif not 1 <= credential_slot <= type_cap.number_of_credential_slots:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="credential_slot_out_of_range",
|
||||
translation_placeholders={
|
||||
"credential_type": cred_type_str,
|
||||
"max_slot": str(type_cap.number_of_credential_slots),
|
||||
},
|
||||
)
|
||||
|
||||
status = await node.access_control.set_credential(
|
||||
user_id, credential_type, credential_slot, credential_data
|
||||
|
||||
@@ -322,9 +322,6 @@
|
||||
"credential_rejected_wrong_uuid": {
|
||||
"message": "The device rejected the credential because the user unique identifier does not match."
|
||||
},
|
||||
"credential_slot_out_of_range": {
|
||||
"message": "Credential slot for {credential_type} must be between 1 and {max_slot}."
|
||||
},
|
||||
"credential_type_not_supported": {
|
||||
"message": "Credential type {credential_type} is not supported on this device"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Helper to track the current http request."""
|
||||
|
||||
from collections.abc import Awaitable, Callable, Container, Mapping
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextvars import ContextVar
|
||||
from http import HTTPStatus
|
||||
import inspect
|
||||
@@ -20,7 +20,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import exceptions
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
from homeassistant.core import Context, HomeAssistant, callback, is_callback
|
||||
from homeassistant.core import Context, HomeAssistant, is_callback
|
||||
from homeassistant.util.json import JSON_ENCODE_EXCEPTIONS, format_unserializable_data
|
||||
|
||||
from .json import find_paths_unserializable_data, json_bytes, json_dumps
|
||||
@@ -55,13 +55,7 @@ def request_handler_factory(
|
||||
|
||||
authenticated = request.get(KEY_AUTHENTICATED, False)
|
||||
|
||||
if view.use_query_token_for_auth and not authenticated:
|
||||
token = request.query.get("token")
|
||||
if token and token in view.get_valid_auth_tokens(request.match_info):
|
||||
_LOGGER.debug("Authenticated request with query token")
|
||||
authenticated = True
|
||||
|
||||
if (view.requires_auth or view.use_query_token_for_auth) and not authenticated:
|
||||
if view.requires_auth and not authenticated:
|
||||
# Import here to avoid circular dependency with network.py
|
||||
from .network import NoURLAvailableError, get_url # noqa: PLC0415
|
||||
|
||||
@@ -135,7 +129,6 @@ class HomeAssistantView:
|
||||
extra_urls: list[str] = []
|
||||
# Views inheriting from this class can override this
|
||||
requires_auth = True
|
||||
use_query_token_for_auth = False
|
||||
cors_allowed = False
|
||||
|
||||
@staticmethod
|
||||
@@ -211,8 +204,3 @@ class HomeAssistantView:
|
||||
if allow_cors:
|
||||
for route in routes:
|
||||
allow_cors(route)
|
||||
|
||||
@callback
|
||||
def get_valid_auth_tokens(self, match_info: Mapping[str, str]) -> Container[str]:
|
||||
"""Return valid auth tokens, which can be used for query token authentication."""
|
||||
return ()
|
||||
|
||||
@@ -699,7 +699,7 @@ def _get_exposed_entities(
|
||||
):
|
||||
# Entity is in area
|
||||
area_names.append(area_entry.name)
|
||||
area_names.extend(area_entry.aliases)
|
||||
area_names.extend(sorted(area_entry.aliases))
|
||||
elif device_entry is not None:
|
||||
# Check device area
|
||||
if (
|
||||
@@ -710,7 +710,7 @@ def _get_exposed_entities(
|
||||
is not None
|
||||
):
|
||||
area_names.append(area_entry.name)
|
||||
area_names.extend(area_entry.aliases)
|
||||
area_names.extend(sorted(area_entry.aliases))
|
||||
|
||||
info: dict[str, Any] = {
|
||||
"names": ", ".join(names),
|
||||
@@ -957,9 +957,9 @@ def _get_cached_action_parameters(
|
||||
aliases = er.async_get_entity_aliases(hass, entity_entry)
|
||||
if aliases:
|
||||
if description:
|
||||
description = description + ". Aliases: " + str(list(aliases))
|
||||
description = description + ". Aliases: " + str(sorted(aliases))
|
||||
else:
|
||||
description = "Aliases: " + str(list(aliases))
|
||||
description = "Aliases: " + str(sorted(aliases))
|
||||
|
||||
parameters_cache.setdefault(domain, {})[action] = (description, parameters)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config entry functions for Home Assistant templates."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.exceptions import TemplateError
|
||||
@@ -104,4 +105,6 @@ class ConfigEntryExtension(BaseTemplateExtension):
|
||||
if config_entry is None:
|
||||
return None
|
||||
|
||||
return getattr(config_entry, attr_name)
|
||||
if isinstance(result := getattr(config_entry, attr_name), Enum):
|
||||
return result.value
|
||||
return result
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.2.7
|
||||
aiodiscover==3.2.4
|
||||
aiodiscover==3.3.2
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.2.0
|
||||
@@ -25,7 +25,7 @@ bleak==3.0.2
|
||||
bluetooth-adapters==2.3.0
|
||||
bluetooth-auto-recovery==1.6.4
|
||||
bluetooth-data-tools==1.29.18
|
||||
cached-ipaddress==1.1.1
|
||||
cached-ipaddress==1.1.2
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
@@ -37,9 +37,9 @@ go2rtc-client==0.4.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==6.8.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.5.0
|
||||
hassil==3.7.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260527.5
|
||||
home-assistant-frontend==20260527.7
|
||||
home-assistant-intents==2026.6.1
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260527.5"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.7"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.2"
|
||||
version = "2026.6.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user