mirror of
https://github.com/home-assistant/core.git
synced 2025-12-09 01:18:02 +00:00
Compare commits
62 Commits
device_tra
...
2025.12.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e29df8eb | ||
|
|
a2b5744696 | ||
|
|
201c3785f5 | ||
|
|
24de26cbf5 | ||
|
|
ac0a544829 | ||
|
|
1a11b92f05 | ||
|
|
ab0811f59f | ||
|
|
68711b2f21 | ||
|
|
886e2b0af1 | ||
|
|
7492b5be75 | ||
|
|
e4f1565e3c | ||
|
|
7f37412199 | ||
|
|
eaef0160a2 | ||
|
|
f049c425ba | ||
|
|
50eee75b8f | ||
|
|
81e47f6844 | ||
|
|
ffebbab020 | ||
|
|
9824bdc1c9 | ||
|
|
a933d4a0eb | ||
|
|
f7f7f9a2de | ||
|
|
aac412f3a8 | ||
|
|
660a14e78d | ||
|
|
4aa3f0a400 | ||
|
|
0b52c806d4 | ||
|
|
bbe27d86a1 | ||
|
|
fb7941df1d | ||
|
|
c46e341941 | ||
|
|
2e3a9e3a90 | ||
|
|
55c5ecd28a | ||
|
|
e50e2487e1 | ||
|
|
74e118f85c | ||
|
|
39a62ec2f6 | ||
|
|
1310efcb07 | ||
|
|
53af592c2c | ||
|
|
023987b805 | ||
|
|
5b8fb607b4 | ||
|
|
252f6716ff | ||
|
|
bf78e28f83 | ||
|
|
22706d02a7 | ||
|
|
5cff0e946a | ||
|
|
6cbe2ed279 | ||
|
|
fb0f5f52b2 | ||
|
|
5c422bb770 | ||
|
|
fd1bc07b8c | ||
|
|
97a019d313 | ||
|
|
8ae8a564c2 | ||
|
|
2f72f57bb7 | ||
|
|
e928e3cb54 | ||
|
|
b0e2109e15 | ||
|
|
b449c6673f | ||
|
|
877ad38ac3 | ||
|
|
229f45feae | ||
|
|
a535d1f4eb | ||
|
|
d4adc00ae6 | ||
|
|
ba141f9d1d | ||
|
|
72be9793a4 | ||
|
|
5ae7cc5f84 | ||
|
|
d01a469b46 | ||
|
|
9f07052874 | ||
|
|
b9bc9d3fc2 | ||
|
|
1e180cd5ee | ||
|
|
dc9cdd13b1 |
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -1354,8 +1354,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ring/ @sdb9696
|
||||
/homeassistant/components/risco/ @OnFreund
|
||||
/tests/components/risco/ @OnFreund
|
||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
||||
/homeassistant/components/rmvtransport/ @cgtobi
|
||||
/tests/components/rmvtransport/ @cgtobi
|
||||
/homeassistant/components/roborock/ @Lash-L @allenporter
|
||||
|
||||
@@ -30,6 +30,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_PASSWORD): selector.TextSelector(
|
||||
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -68,34 +69,19 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass,
|
||||
cookie_jar=CookieJar(quote_cookie=False),
|
||||
),
|
||||
account_number=user_input.get(CONF_ACCOUNT_NUMBER),
|
||||
account_number=user_input[CONF_ACCOUNT_NUMBER],
|
||||
)
|
||||
)
|
||||
if isinstance(validation_response, BaseAuth):
|
||||
account_number = (
|
||||
user_input.get(CONF_ACCOUNT_NUMBER)
|
||||
or validation_response.account_number
|
||||
)
|
||||
await self.async_set_unique_id(account_number)
|
||||
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=account_number,
|
||||
title=user_input[CONF_ACCOUNT_NUMBER],
|
||||
data={
|
||||
**user_input,
|
||||
CONF_ACCESS_TOKEN: validation_response.refresh_token,
|
||||
CONF_ACCOUNT_NUMBER: account_number,
|
||||
},
|
||||
)
|
||||
if validation_response == "smart_meter_unavailable":
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
|
||||
}
|
||||
),
|
||||
errors={"base": validation_response},
|
||||
)
|
||||
errors["base"] = validation_response
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==2.1.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
|
||||
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.1"]
|
||||
}
|
||||
|
||||
@@ -102,6 +102,12 @@ class ConfiguredDoorBird:
|
||||
"""Get token for device."""
|
||||
return self._token
|
||||
|
||||
def _get_hass_url(self) -> str:
|
||||
"""Get the Home Assistant URL for this device."""
|
||||
if custom_url := self.custom_url:
|
||||
return custom_url
|
||||
return get_url(self._hass, prefer_external=False)
|
||||
|
||||
async def async_register_events(self) -> None:
|
||||
"""Register events on device."""
|
||||
if not self.door_station_events:
|
||||
@@ -146,13 +152,7 @@ class ConfiguredDoorBird:
|
||||
|
||||
async def _async_register_events(self) -> dict[str, Any]:
|
||||
"""Register events on device."""
|
||||
# Override url if another is specified in the configuration
|
||||
if custom_url := self.custom_url:
|
||||
hass_url = custom_url
|
||||
else:
|
||||
# Get the URL of this server
|
||||
hass_url = get_url(self._hass, prefer_external=False)
|
||||
|
||||
hass_url = self._get_hass_url()
|
||||
http_fav = await self._async_get_http_favorites()
|
||||
if any(
|
||||
# Note that a list comp is used here to ensure all
|
||||
@@ -191,10 +191,14 @@ class ConfiguredDoorBird:
|
||||
self._get_event_name(event): event_type
|
||||
for event, event_type in DEFAULT_EVENT_TYPES
|
||||
}
|
||||
hass_url = self._get_hass_url()
|
||||
for identifier, data in http_fav.items():
|
||||
title: str | None = data.get("title")
|
||||
if not title or not title.startswith("Home Assistant"):
|
||||
continue
|
||||
value: str | None = data.get("value")
|
||||
if not value or not value.startswith(hass_url):
|
||||
continue # Not our favorite - different HA instance or stale
|
||||
event = title.partition("(")[2].strip(")")
|
||||
if input_type := favorite_input_type.get(identifier):
|
||||
events.append(DoorbirdEvent(event, input_type))
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.5"]
|
||||
"requirements": ["evohome-async==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"winter_mode": {}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251203.0"]
|
||||
"requirements": ["home-assistant-frontend==20251203.2"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"preview_features": {
|
||||
"winter_mode": {
|
||||
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️",
|
||||
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in labs settings.",
|
||||
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in labs settings.",
|
||||
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.",
|
||||
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.",
|
||||
"enable_confirmation": "Snowflakes will start falling on your screen. You can turn this off at any time in Labs settings.",
|
||||
"name": "Winter mode"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -51,9 +51,9 @@ async def _validate_input(
|
||||
description_placeholders: dict[str, str],
|
||||
) -> bool:
|
||||
try:
|
||||
await api.async_air_quality(
|
||||
await api.async_get_current_conditions(
|
||||
lat=user_input[CONF_LOCATION][CONF_LATITUDE],
|
||||
long=user_input[CONF_LOCATION][CONF_LONGITUDE],
|
||||
lon=user_input[CONF_LOCATION][CONF_LONGITUDE],
|
||||
)
|
||||
except GoogleAirQualityApiError as err:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Final
|
||||
|
||||
from google_air_quality_api.api import GoogleAirQualityApi
|
||||
from google_air_quality_api.exceptions import GoogleAirQualityApiError
|
||||
from google_air_quality_api.model import AirQualityData
|
||||
from google_air_quality_api.model import AirQualityCurrentConditionsData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
@@ -23,7 +23,9 @@ UPDATE_INTERVAL: Final = timedelta(hours=1)
|
||||
type GoogleAirQualityConfigEntry = ConfigEntry[GoogleAirQualityRuntimeData]
|
||||
|
||||
|
||||
class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
|
||||
class GoogleAirQualityUpdateCoordinator(
|
||||
DataUpdateCoordinator[AirQualityCurrentConditionsData]
|
||||
):
|
||||
"""Coordinator for fetching Google AirQuality data."""
|
||||
|
||||
config_entry: GoogleAirQualityConfigEntry
|
||||
@@ -48,10 +50,10 @@ class GoogleAirQualityUpdateCoordinator(DataUpdateCoordinator[AirQualityData]):
|
||||
self.lat = subentry.data[CONF_LATITUDE]
|
||||
self.long = subentry.data[CONF_LONGITUDE]
|
||||
|
||||
async def _async_update_data(self) -> AirQualityData:
|
||||
async def _async_update_data(self) -> AirQualityCurrentConditionsData:
|
||||
"""Fetch air quality data for this coordinate."""
|
||||
try:
|
||||
return await self.client.async_air_quality(self.lat, self.long)
|
||||
return await self.client.async_get_current_conditions(self.lat, self.long)
|
||||
except GoogleAirQualityApiError as ex:
|
||||
_LOGGER.debug("Cannot fetch air quality data: %s", str(ex))
|
||||
raise UpdateFailed(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_air_quality_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["google_air_quality_api==1.1.3"]
|
||||
"requirements": ["google_air_quality_api==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from google_air_quality_api.model import AirQualityData
|
||||
from google_air_quality_api.model import AirQualityCurrentConditionsData
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -33,15 +33,17 @@ PARALLEL_UPDATES = 0
|
||||
class AirQualitySensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Air Quality sensor entity."""
|
||||
|
||||
exists_fn: Callable[[AirQualityData], bool] = lambda _: True
|
||||
options_fn: Callable[[AirQualityData], list[str] | None] = lambda _: None
|
||||
value_fn: Callable[[AirQualityData], StateType]
|
||||
native_unit_of_measurement_fn: Callable[[AirQualityData], str | None] = (
|
||||
exists_fn: Callable[[AirQualityCurrentConditionsData], bool] = lambda _: True
|
||||
options_fn: Callable[[AirQualityCurrentConditionsData], list[str] | None] = (
|
||||
lambda _: None
|
||||
)
|
||||
translation_placeholders_fn: Callable[[AirQualityData], dict[str, str]] | None = (
|
||||
None
|
||||
)
|
||||
value_fn: Callable[[AirQualityCurrentConditionsData], StateType]
|
||||
native_unit_of_measurement_fn: Callable[
|
||||
[AirQualityCurrentConditionsData], str | None
|
||||
] = lambda _: None
|
||||
translation_placeholders_fn: (
|
||||
Callable[[AirQualityCurrentConditionsData], dict[str, str]] | None
|
||||
) = None
|
||||
|
||||
|
||||
AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
|
||||
@@ -149,6 +149,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
description_placeholders={"example_image_path": "/config/www/image.jpg"},
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ CONF_CHAT_MODEL = "chat_model"
|
||||
RECOMMENDED_CHAT_MODEL = "models/gemini-2.5-flash"
|
||||
RECOMMENDED_STT_MODEL = RECOMMENDED_CHAT_MODEL
|
||||
RECOMMENDED_TTS_MODEL = "models/gemini-2.5-flash-preview-tts"
|
||||
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image-preview"
|
||||
RECOMMENDED_IMAGE_MODEL = "models/gemini-2.5-flash-image"
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
RECOMMENDED_TEMPERATURE = 1.0
|
||||
CONF_TOP_P = "top_p"
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
"fields": {
|
||||
"filenames": {
|
||||
"description": "Attachments to add to the prompt (images, PDFs, etc)",
|
||||
"example": "/config/www/image.jpg",
|
||||
"example": "{example_image_path}",
|
||||
"name": "Attachment filenames"
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -159,4 +159,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
description_placeholders={"example_image_path": "/config/www/image.jpg"},
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"filename": {
|
||||
"description": "Path to the image or video to upload.",
|
||||
"example": "/config/www/image.jpg",
|
||||
"example": "{example_image_path}",
|
||||
"name": "Filename"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from HueBLE import HueBleLight
|
||||
from HueBLE import ConnectionError, HueBleError, HueBleLight
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
async_ble_device_from_address,
|
||||
@@ -38,8 +38,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bo
|
||||
|
||||
light = HueBleLight(ble_device)
|
||||
|
||||
if not await light.connect() or not await light.poll_state():
|
||||
raise ConfigEntryNotReady("Device found but unable to connect.")
|
||||
try:
|
||||
await light.connect()
|
||||
await light.poll_state()
|
||||
except ConnectionError as e:
|
||||
raise ConfigEntryNotReady("Device found but unable to connect.") from e
|
||||
except HueBleError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
"Device found and connected but unable to poll values from it."
|
||||
) from e
|
||||
|
||||
entry.runtime_data = light
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from enum import Enum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from HueBLE import HueBleLight
|
||||
from HueBLE import ConnectionError, HueBleError, HueBleLight, PairingError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
@@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN, URL_PAIRING_MODE
|
||||
from .const import DOMAIN, URL_FACTORY_RESET, URL_PAIRING_MODE
|
||||
from .light import get_available_color_modes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -41,32 +41,22 @@ async def validate_input(hass: HomeAssistant, address: str) -> Error | None:
|
||||
|
||||
try:
|
||||
light = HueBleLight(ble_device)
|
||||
|
||||
await light.connect()
|
||||
get_available_color_modes(light)
|
||||
await light.poll_state()
|
||||
|
||||
if light.authenticated is None:
|
||||
_LOGGER.warning(
|
||||
"Unable to determine if light authenticated, proceeding anyway"
|
||||
)
|
||||
elif not light.authenticated:
|
||||
return Error.INVALID_AUTH
|
||||
|
||||
if not light.connected:
|
||||
return Error.CANNOT_CONNECT
|
||||
|
||||
try:
|
||||
get_available_color_modes(light)
|
||||
except HomeAssistantError:
|
||||
return Error.NOT_SUPPORTED
|
||||
|
||||
_, errors = await light.poll_state()
|
||||
if len(errors) != 0:
|
||||
_LOGGER.warning("Errors raised when connecting to light: %s", errors)
|
||||
return Error.CANNOT_CONNECT
|
||||
|
||||
except Exception:
|
||||
except ConnectionError as e:
|
||||
_LOGGER.exception("Error connecting to light")
|
||||
return (
|
||||
Error.INVALID_AUTH
|
||||
if type(e.__cause__) is PairingError
|
||||
else Error.CANNOT_CONNECT
|
||||
)
|
||||
except HueBleError:
|
||||
_LOGGER.exception("Unexpected error validating light connection")
|
||||
return Error.UNKNOWN
|
||||
except HomeAssistantError:
|
||||
return Error.NOT_SUPPORTED
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
@@ -129,6 +119,7 @@ class HueBleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_NAME: self._discovery_info.name,
|
||||
CONF_MAC: self._discovery_info.address,
|
||||
"url_pairing_mode": URL_PAIRING_MODE,
|
||||
"url_factory_reset": URL_FACTORY_RESET,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
DOMAIN = "hue_ble"
|
||||
URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup"
|
||||
URL_FACTORY_RESET = "https://www.philips-hue.com/en-gb/support/article/how-to-factory-reset-philips-hue-lights/000004"
|
||||
|
||||
@@ -113,7 +113,7 @@ class HueBLELight(LightEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Fetch latest state from light and make available via properties."""
|
||||
await self._api.poll_state(run_callbacks=True)
|
||||
await self._api.poll_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set properties then turn the light on."""
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["bleak", "HueBLE"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["HueBLE==1.0.8"]
|
||||
"requirements": ["HueBLE==2.1.0"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode})."
|
||||
"description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode}) or has been [factory reset]({url_factory_reset})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,10 @@ in the Home Assistant Labs UI for users to enable or disable.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
|
||||
from homeassistant.const import EVENT_LABS_UPDATED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.generated.labs import LABS_PREVIEW_FEATURES
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -19,6 +18,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_custom_components
|
||||
|
||||
from .const import DOMAIN, LABS_DATA, STORAGE_KEY, STORAGE_VERSION
|
||||
from .helpers import async_is_preview_feature_enabled, async_listen
|
||||
from .models import (
|
||||
EventLabsUpdatedData,
|
||||
LabPreviewFeature,
|
||||
@@ -135,55 +135,3 @@ async def _async_scan_all_preview_features(
|
||||
|
||||
_LOGGER.debug("Loaded %d total lab preview features", len(preview_features))
|
||||
return preview_features
|
||||
|
||||
|
||||
@callback
|
||||
def async_is_preview_feature_enabled(
|
||||
hass: HomeAssistant, domain: str, preview_feature: str
|
||||
) -> bool:
|
||||
"""Check if a lab preview feature is enabled.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
domain: Integration domain
|
||||
preview_feature: Preview feature name
|
||||
|
||||
Returns:
|
||||
True if the preview feature is enabled, False otherwise
|
||||
"""
|
||||
if LABS_DATA not in hass.data:
|
||||
return False
|
||||
|
||||
labs_data = hass.data[LABS_DATA]
|
||||
return (domain, preview_feature) in labs_data.data.preview_feature_status
|
||||
|
||||
|
||||
@callback
|
||||
def async_listen(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
preview_feature: str,
|
||||
listener: Callable[[], None],
|
||||
) -> Callable[[], None]:
|
||||
"""Listen for changes to a specific preview feature.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
domain: Integration domain
|
||||
preview_feature: Preview feature name
|
||||
listener: Callback to invoke when the preview feature is toggled
|
||||
|
||||
Returns:
|
||||
Callable to unsubscribe from the listener
|
||||
"""
|
||||
|
||||
@callback
|
||||
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
|
||||
"""Handle labs feature update event."""
|
||||
if (
|
||||
event.data["domain"] == domain
|
||||
and event.data["preview_feature"] == preview_feature
|
||||
):
|
||||
listener()
|
||||
|
||||
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)
|
||||
|
||||
63
homeassistant/components/labs/helpers.py
Normal file
63
homeassistant/components/labs/helpers.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Helper functions for the Home Assistant Labs integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.const import EVENT_LABS_UPDATED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
|
||||
from .const import LABS_DATA
|
||||
from .models import EventLabsUpdatedData
|
||||
|
||||
|
||||
@callback
|
||||
def async_is_preview_feature_enabled(
|
||||
hass: HomeAssistant, domain: str, preview_feature: str
|
||||
) -> bool:
|
||||
"""Check if a lab preview feature is enabled.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
domain: Integration domain
|
||||
preview_feature: Preview feature name
|
||||
|
||||
Returns:
|
||||
True if the preview feature is enabled, False otherwise
|
||||
"""
|
||||
if LABS_DATA not in hass.data:
|
||||
return False
|
||||
|
||||
labs_data = hass.data[LABS_DATA]
|
||||
return (domain, preview_feature) in labs_data.data.preview_feature_status
|
||||
|
||||
|
||||
@callback
|
||||
def async_listen(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
preview_feature: str,
|
||||
listener: Callable[[], None],
|
||||
) -> Callable[[], None]:
|
||||
"""Listen for changes to a specific preview feature.
|
||||
|
||||
Args:
|
||||
hass: HomeAssistant instance
|
||||
domain: Integration domain
|
||||
preview_feature: Preview feature name
|
||||
listener: Callback to invoke when the preview feature is toggled
|
||||
|
||||
Returns:
|
||||
Callable to unsubscribe from the listener
|
||||
"""
|
||||
|
||||
@callback
|
||||
def _async_feature_updated(event: Event[EventLabsUpdatedData]) -> None:
|
||||
"""Handle labs feature update event."""
|
||||
if (
|
||||
event.data["domain"] == domain
|
||||
and event.data["preview_feature"] == preview_feature
|
||||
):
|
||||
listener()
|
||||
|
||||
return hass.bus.async_listen(EVENT_LABS_UPDATED, _async_feature_updated)
|
||||
@@ -12,6 +12,7 @@ from homeassistant.const import EVENT_LABS_UPDATED
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import LABS_DATA
|
||||
from .helpers import async_is_preview_feature_enabled, async_listen
|
||||
from .models import EventLabsUpdatedData
|
||||
|
||||
|
||||
@@ -20,6 +21,7 @@ def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the number websocket API."""
|
||||
websocket_api.async_register_command(hass, websocket_list_preview_features)
|
||||
websocket_api.async_register_command(hass, websocket_update_preview_feature)
|
||||
websocket_api.async_register_command(hass, websocket_subscribe_feature)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -108,3 +110,52 @@ async def websocket_update_preview_feature(
|
||||
hass.bus.async_fire(EVENT_LABS_UPDATED, event_data)
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "labs/subscribe",
|
||||
vol.Required("domain"): str,
|
||||
vol.Required("preview_feature"): str,
|
||||
}
|
||||
)
|
||||
def websocket_subscribe_feature(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Subscribe to a specific lab preview feature updates."""
|
||||
domain = msg["domain"]
|
||||
preview_feature_key = msg["preview_feature"]
|
||||
labs_data = hass.data[LABS_DATA]
|
||||
|
||||
preview_feature_id = f"{domain}.{preview_feature_key}"
|
||||
|
||||
if preview_feature_id not in labs_data.preview_features:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_FOUND,
|
||||
f"Preview feature {preview_feature_id} not found",
|
||||
)
|
||||
return
|
||||
|
||||
preview_feature = labs_data.preview_features[preview_feature_id]
|
||||
|
||||
@callback
|
||||
def send_event() -> None:
|
||||
"""Send feature state to client."""
|
||||
enabled = async_is_preview_feature_enabled(hass, domain, preview_feature_key)
|
||||
connection.send_message(
|
||||
websocket_api.event_message(
|
||||
msg["id"],
|
||||
preview_feature.to_dict(enabled=enabled),
|
||||
)
|
||||
)
|
||||
|
||||
connection.subscriptions[msg["id"]] = async_listen(
|
||||
hass, domain, preview_feature_key, send_event
|
||||
)
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
send_event()
|
||||
|
||||
@@ -108,6 +108,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
SERVICE_MESSAGE,
|
||||
_async_service_message,
|
||||
schema=SERVICE_MESSAGE_SCHEMA,
|
||||
description_placeholders={"icons_url": "https://developer.lametric.com/icons"},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"name": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"icon": {
|
||||
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: https://developer.lametric.com/icons.",
|
||||
"description": "The ID number of the icon or animation to display. List of all icons and their IDs can be found at: {icons_url}.",
|
||||
"name": "Icon ID"
|
||||
},
|
||||
"icon_type": {
|
||||
|
||||
@@ -98,7 +98,11 @@ class LutronCasetaSmartAwaySwitch(LutronCasetaEntity, SwitchEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
self._smartbridge.add_smart_away_subscriber(self._handle_bridge_update)
|
||||
self._smartbridge.add_smart_away_subscriber(self._handle_smart_away_update)
|
||||
|
||||
def _handle_smart_away_update(self, smart_away_state: str | None = None) -> None:
|
||||
"""Handle updated smart away state from the bridge."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn Smart Away on."""
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.11.12"],
|
||||
"requirements": ["yt-dlp[default]==2025.12.08"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ class ProgramPhaseWashingMachine(MieleEnum, missing_to_none=True):
|
||||
drying = 280
|
||||
disinfecting = 285
|
||||
flex_load_active = 11047
|
||||
automatic_start = 11044
|
||||
|
||||
|
||||
class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
|
||||
@@ -451,19 +452,19 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for washing machines."""
|
||||
|
||||
no_program = 0, -1
|
||||
cottons = 1
|
||||
cottons = 1, 10001
|
||||
minimum_iron = 3
|
||||
delicates = 4
|
||||
woollens = 8
|
||||
silks = 9
|
||||
delicates = 4, 10022
|
||||
woollens = 8, 10040
|
||||
silks = 9, 10042
|
||||
starch = 17
|
||||
rinse = 18
|
||||
drain_spin = 21
|
||||
curtains = 22
|
||||
shirts = 23
|
||||
rinse = 18, 10058
|
||||
drain_spin = 21, 10036
|
||||
curtains = 22, 10055
|
||||
shirts = 23, 10038
|
||||
denim = 24, 123
|
||||
proofing = 27
|
||||
sportswear = 29
|
||||
proofing = 27, 10057
|
||||
sportswear = 29, 10052
|
||||
automatic_plus = 31
|
||||
outerwear = 37
|
||||
pillows = 39
|
||||
@@ -472,19 +473,29 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
|
||||
rinse_out_lint = 48 # washer-dryer
|
||||
dark_garments = 50
|
||||
separate_rinse_starch = 52
|
||||
first_wash = 53
|
||||
first_wash = 53, 10053
|
||||
cottons_hygiene = 69
|
||||
steam_care = 75 # washer-dryer
|
||||
freshen_up = 76 # washer-dryer
|
||||
trainers = 77
|
||||
clean_machine = 91
|
||||
down_duvets = 95
|
||||
express_20 = 122
|
||||
trainers = 77, 10056
|
||||
clean_machine = 91, 10067
|
||||
down_duvets = 95, 10050
|
||||
express_20 = 122, 10029
|
||||
down_filled_items = 129
|
||||
cottons_eco = 133
|
||||
quick_power_wash = 146, 10031
|
||||
eco_40_60 = 190, 10007
|
||||
normal = 10001
|
||||
bed_linen = 10047
|
||||
easy_care = 10016
|
||||
dark_jeans = 10048
|
||||
outdoor_garments = 10049
|
||||
game_pieces = 10070
|
||||
stuffed_toys = 10069
|
||||
pre_ironing = 10059
|
||||
trainers_refresh = 10066
|
||||
smartmatic = 10068
|
||||
cottonrepair = 10065
|
||||
powerfresh = 10075
|
||||
|
||||
|
||||
class DishWasherProgramId(MieleEnum, missing_to_none=True):
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pymiele==0.6.0"],
|
||||
"requirements": ["pymiele==0.6.1"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
"cook_bacon": "Cook bacon",
|
||||
"cool_air": "Cool air",
|
||||
"corn_on_the_cob": "Corn on the cob",
|
||||
"cottonrepair": "CottonRepair",
|
||||
"cottons": "Cottons",
|
||||
"cottons_eco": "Cottons ECO",
|
||||
"cottons_hygiene": "Cottons hygiene",
|
||||
@@ -440,6 +441,7 @@
|
||||
"custom_program_8": "Custom program 8",
|
||||
"custom_program_9": "Custom program 9",
|
||||
"dark_garments": "Dark garments",
|
||||
"dark_jeans": "Dark/jeans",
|
||||
"dark_mixed_grain_bread": "Dark mixed grain bread",
|
||||
"decrystallise_honey": "Decrystallize honey",
|
||||
"defrost": "Defrost",
|
||||
@@ -457,6 +459,7 @@
|
||||
"drop_cookies_2_trays": "Drop cookies (2 trays)",
|
||||
"duck": "Duck",
|
||||
"dutch_hash": "Dutch hash",
|
||||
"easy_care": "Easy care",
|
||||
"eco": "ECO",
|
||||
"eco_40_60": "ECO 40-60",
|
||||
"eco_fan_heat": "ECO fan heat",
|
||||
@@ -487,6 +490,7 @@
|
||||
"fruit_streusel_cake": "Fruit streusel cake",
|
||||
"fruit_tea": "Fruit tea",
|
||||
"full_grill": "Full grill",
|
||||
"game_pieces": "Game pieces",
|
||||
"gentle": "Gentle",
|
||||
"gentle_denim": "Gentle denim",
|
||||
"gentle_minimum_iron": "Gentle minimum iron",
|
||||
@@ -607,6 +611,7 @@
|
||||
"oats_cracked": "Oats (cracked)",
|
||||
"oats_whole": "Oats (whole)",
|
||||
"osso_buco": "Osso buco",
|
||||
"outdoor_garments": "Outdoor garments",
|
||||
"outerwear": "Outerwear",
|
||||
"oyster_mushroom_diced": "Oyster mushroom (diced)",
|
||||
"oyster_mushroom_strips": "Oyster mushroom (strips)",
|
||||
@@ -713,8 +718,10 @@
|
||||
"potatoes_waxy_whole_small": "Potatoes (waxy, whole, small)",
|
||||
"poularde_breast": "Poularde breast",
|
||||
"poularde_whole": "Poularde (whole)",
|
||||
"power_fresh": "PowerFresh",
|
||||
"power_wash": "PowerWash",
|
||||
"prawns": "Prawns",
|
||||
"pre_ironing": "Pre-ironing",
|
||||
"proofing": "Proofing",
|
||||
"prove_15_min": "Prove for 15 min",
|
||||
"prove_30_min": "Prove for 30 min",
|
||||
@@ -807,6 +814,7 @@
|
||||
"simiao_rapid_steam_cooking": "Simiao (rapid steam cooking)",
|
||||
"simiao_steam_cooking": "Simiao (steam cooking)",
|
||||
"small_shrimps": "Small shrimps",
|
||||
"smartmatic": "SmartMatic",
|
||||
"smoothing": "Smoothing",
|
||||
"snow_pea": "Snow pea",
|
||||
"soak": "Soak",
|
||||
@@ -833,6 +841,7 @@
|
||||
"sterilize_crockery": "Sterilize crockery",
|
||||
"stollen": "Stollen",
|
||||
"stuffed_cabbage": "Stuffed cabbage",
|
||||
"stuffed_toys": "Stuffed toys",
|
||||
"sweat_onions": "Sweat onions",
|
||||
"swede_cut_into_batons": "Swede (cut into batons)",
|
||||
"swede_diced": "Swede (diced)",
|
||||
@@ -855,6 +864,7 @@
|
||||
"top_heat": "Top heat",
|
||||
"tortellini_fresh": "Tortellini (fresh)",
|
||||
"trainers": "Trainers",
|
||||
"trainers_refresh": "Trainers refresh",
|
||||
"treacle_sponge_pudding_one_large": "Treacle sponge pudding (one large)",
|
||||
"treacle_sponge_pudding_several_small": "Treacle sponge pudding (several small)",
|
||||
"trout": "Trout",
|
||||
@@ -935,6 +945,7 @@
|
||||
"2nd_grinding": "2nd grinding",
|
||||
"2nd_pre_brewing": "2nd pre-brewing",
|
||||
"anti_crease": "Anti-crease",
|
||||
"automatic_start": "Automatic start",
|
||||
"blocked_brushes": "Brushes blocked",
|
||||
"blocked_drive_wheels": "Drive wheels blocked",
|
||||
"blocked_front_wheel": "Front wheel blocked",
|
||||
|
||||
@@ -163,9 +163,6 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.exception("Unexpected exception during add-on discovery")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
if not server_info.onboard_done:
|
||||
return self.async_abort(reason="server_not_ready")
|
||||
|
||||
# We trust the token from hassio discovery and validate it during setup
|
||||
self.token = discovery_info.config["auth_token"]
|
||||
|
||||
@@ -226,11 +223,6 @@ class MusicAssistantConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.debug("Ignoring add-on server in zeroconf discovery")
|
||||
return self.async_abort(reason="already_discovered_addon")
|
||||
|
||||
# Ignore servers that have not completed onboarding yet
|
||||
if not server_info.onboard_done:
|
||||
LOGGER.debug("Ignoring server that hasn't completed onboarding")
|
||||
return self.async_abort(reason="server_not_ready")
|
||||
|
||||
self.url = server_info.base_url
|
||||
self.server_info = server_info
|
||||
|
||||
|
||||
@@ -129,4 +129,5 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
description_placeholders={"example_image_path": "/config/www/image.jpg"},
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
},
|
||||
"filename": {
|
||||
"description": "Path to the file to upload.",
|
||||
"example": "/config/www/image.jpg",
|
||||
"example": "{example_image_path}",
|
||||
"name": "Filename"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["oralb_ble"],
|
||||
"requirements": ["oralb-ble==0.17.6"]
|
||||
"requirements": ["oralb-ble==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -101,7 +102,18 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
except OSError:
|
||||
_LOGGER.error("Pilight send failed for %s", str(message_data))
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA)
|
||||
def _register_service() -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_NAME,
|
||||
send_code,
|
||||
schema=RF_CODE_SCHEMA,
|
||||
description_placeholders={
|
||||
"pilight_protocols_docs_url": "https://manual.pilight.org/protocols/index.html"
|
||||
},
|
||||
)
|
||||
|
||||
run_callback_threadsafe(hass.loop, _register_service).result()
|
||||
|
||||
# Publish received codes on the HA event bus
|
||||
# A whitelist of codes to be published in the event bus
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Sends RF code to Pilight device.",
|
||||
"fields": {
|
||||
"protocol": {
|
||||
"description": "Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports.",
|
||||
"description": "Protocol that Pilight recognizes. See {pilight_protocols_docs_url} for supported protocols and additional parameters that each protocol supports.",
|
||||
"name": "Protocol"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,8 +54,11 @@ from .const import (
|
||||
)
|
||||
from .coordinator import RainMachineDataUpdateCoordinator
|
||||
|
||||
DEFAULT_SSL = True
|
||||
API_URL_REFERENCE = (
|
||||
"https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post"
|
||||
)
|
||||
|
||||
DEFAULT_SSL = True
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -455,7 +458,15 @@ async def async_setup_entry( # noqa: C901
|
||||
):
|
||||
if hass.services.has_service(DOMAIN, service_name):
|
||||
continue
|
||||
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
service_name,
|
||||
method,
|
||||
schema=schema,
|
||||
description_placeholders={
|
||||
"api_url": API_URL_REFERENCE,
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
"name": "Push flow meter data"
|
||||
},
|
||||
"push_weather_data": {
|
||||
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: https://rainmachine.docs.apiary.io/#reference/weather-services/parserdata/post.",
|
||||
"description": "Sends weather data from Home Assistant to the RainMachine device.\nLocal Weather Push service should be enabled from Settings > Weather > Developer tab for RainMachine to consider the values being sent. Units must be sent in metric; no conversions are performed by the integration.\nSee details of RainMachine API here: {api_url}",
|
||||
"fields": {
|
||||
"condition": {
|
||||
"description": "Current weather condition code (WNUM).",
|
||||
|
||||
@@ -422,6 +422,8 @@ class ReolinkHost:
|
||||
"name": self._api.nvr_name,
|
||||
"base_url": self._base_url,
|
||||
"network_link": "https://my.home-assistant.io/redirect/network/",
|
||||
"example_ip": "192.168.1.10",
|
||||
"example_url": "http://192.168.1.10:8123",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -436,6 +438,8 @@ class ReolinkHost:
|
||||
translation_placeholders={
|
||||
"base_url": self._base_url,
|
||||
"network_link": "https://my.home-assistant.io/redirect/network/",
|
||||
"example_ip": "192.168.1.10",
|
||||
"example_url": "http://192.168.1.10:8123",
|
||||
},
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.17.0"]
|
||||
"requirements": ["reolink-aio==0.17.1"]
|
||||
}
|
||||
|
||||
@@ -1004,7 +1004,7 @@
|
||||
"title": "Reolink firmware update required"
|
||||
},
|
||||
"https_webhook": {
|
||||
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device",
|
||||
"description": "Reolink products can not push motion events to an HTTPS address (SSL), please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}). The current (local) address is: `{base_url}`, a valid address could, for example, be `{example_url}` where `{example_ip}` is the IP of the Home Assistant device",
|
||||
"title": "Reolink webhook URL uses HTTPS (SSL)"
|
||||
},
|
||||
"password_too_long": {
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"title": "Reolink incompatible with global SSL certificate"
|
||||
},
|
||||
"webhook_url": {
|
||||
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `http://192.168.1.10:8123` where `192.168.1.10` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
|
||||
"description": "Did not receive initial ONVIF state from {name}. Most likely, the Reolink camera can not reach the current (local) Home Assistant URL `{base_url}`, please configure a (local) HTTP address under \"Home Assistant URL\" in the [network settings]({network_link}) that points to Home Assistant. For example `{example_url}` where `{example_ip}` is the IP of the Home Assistant device. Also, make sure the Reolink camera can reach that URL. Using fast motion/AI state polling until the first ONVIF push is received.",
|
||||
"title": "Reolink webhook URL unreachable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
"""The Rituals Perfume Genie integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from pyrituals import Account, Diffuser
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from pyrituals import Account, AuthenticationException, Diffuser
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import ACCOUNT_HASH, DOMAIN, UPDATE_INTERVAL
|
||||
from .coordinator import RitualsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NUMBER,
|
||||
@@ -26,12 +29,38 @@ PLATFORMS = [
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Rituals Perfume Genie from a config entry."""
|
||||
# Initiate reauth for old config entries which don't have username / password in the entry data
|
||||
if CONF_EMAIL not in entry.data or CONF_PASSWORD not in entry.data:
|
||||
raise ConfigEntryAuthFailed("Missing credentials")
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
account = Account(session=session, account_hash=entry.data[ACCOUNT_HASH])
|
||||
|
||||
account = Account(
|
||||
email=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
# Authenticate first so API token/cookies are available for subsequent calls
|
||||
await account.authenticate()
|
||||
account_devices = await account.get_devices()
|
||||
except aiohttp.ClientError as err:
|
||||
|
||||
except AuthenticationException as err:
|
||||
# Credentials invalid/expired -> raise AuthFailed to trigger reauth flow
|
||||
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
|
||||
except ClientResponseError as err:
|
||||
_LOGGER.debug(
|
||||
"HTTP error during Rituals setup: status=%s, url=%s, headers=%s",
|
||||
err.status,
|
||||
err.request_info,
|
||||
dict(err.headers or {}),
|
||||
)
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
# Migrate old unique_ids to the new format
|
||||
@@ -45,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Create a coordinator for each diffuser
|
||||
coordinators = {
|
||||
diffuser.hublot: RitualsDataUpdateCoordinator(
|
||||
hass, entry, diffuser, update_interval
|
||||
hass, entry, account, diffuser, update_interval
|
||||
)
|
||||
for diffuser in account_devices
|
||||
}
|
||||
@@ -106,3 +135,14 @@ def async_migrate_entities_unique_ids(
|
||||
registry_entry.entity_id,
|
||||
new_unique_id=f"{diffuser.hublot}-{new_unique_id}",
|
||||
)
|
||||
|
||||
|
||||
# Migration helpers for API v2
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate config entry to version 2: drop legacy ACCOUNT_HASH and bump version."""
|
||||
if entry.version < 2:
|
||||
data = dict(entry.data)
|
||||
data.pop(ACCOUNT_HASH, None)
|
||||
hass.config_entries.async_update_entry(entry, data=data, version=2)
|
||||
return True
|
||||
return True
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from pyrituals import Account, AuthenticationException
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,9 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import ACCOUNT_HASH, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -28,39 +26,88 @@ DATA_SCHEMA = vol.Schema(
|
||||
class RitualsPerfumeGenieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Rituals Perfume Genie."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)
|
||||
|
||||
errors = {}
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
account = Account(user_input[CONF_EMAIL], user_input[CONF_PASSWORD], session)
|
||||
|
||||
try:
|
||||
await account.authenticate()
|
||||
except ClientResponseError:
|
||||
_LOGGER.exception("Unexpected response")
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(account.email)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=account.email,
|
||||
data={ACCOUNT_HASH: account.account_hash},
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
account = Account(
|
||||
email=user_input[CONF_EMAIL],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
await account.authenticate()
|
||||
except AuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_EMAIL])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL],
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle re-authentication with Rituals."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Form to log in again."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert reauth_entry.unique_id is not None
|
||||
|
||||
if user_input:
|
||||
session = async_get_clientsession(self.hass)
|
||||
account = Account(
|
||||
email=reauth_entry.unique_id,
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
await account.authenticate()
|
||||
except AuthenticationException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ClientError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data={
|
||||
CONF_EMAIL: reauth_entry.unique_id,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
reauth_entry.data,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from datetime import timedelta
|
||||
|
||||
DOMAIN = "rituals_perfume_genie"
|
||||
|
||||
# Old (API V1)
|
||||
ACCOUNT_HASH = "account_hash"
|
||||
|
||||
# The API provided by Rituals is currently rate limited to 30 requests
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyrituals import Diffuser
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from pyrituals import Account, AuthenticationException, Diffuser
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -23,10 +25,12 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
account: Account,
|
||||
diffuser: Diffuser,
|
||||
update_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize global Rituals Perfume Genie data updater."""
|
||||
self.account = account
|
||||
self.diffuser = diffuser
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -37,5 +41,36 @@ class RitualsDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from Rituals."""
|
||||
await self.diffuser.update_data()
|
||||
"""Fetch data from Rituals, with one silent re-auth on 401.
|
||||
|
||||
If silent re-auth also fails, raise ConfigEntryAuthFailed to trigger reauth flow.
|
||||
Other HTTP/network errors are wrapped in UpdateFailed so HA can retry.
|
||||
"""
|
||||
try:
|
||||
await self.diffuser.update_data()
|
||||
except (AuthenticationException, ClientResponseError) as err:
|
||||
# Treat 401/403 like AuthenticationException → one silent re-auth, single retry
|
||||
if isinstance(err, ClientResponseError) and (status := err.status) not in (
|
||||
401,
|
||||
403,
|
||||
):
|
||||
# Non-auth HTTP error → let HA retry
|
||||
raise UpdateFailed(f"HTTP {status}") from err
|
||||
|
||||
self.logger.debug(
|
||||
"Auth issue detected (%r). Attempting silent re-auth.", err
|
||||
)
|
||||
try:
|
||||
await self.account.authenticate()
|
||||
await self.diffuser.update_data()
|
||||
except AuthenticationException as err2:
|
||||
# Credentials invalid → trigger HA reauth
|
||||
raise ConfigEntryAuthFailed from err2
|
||||
except ClientResponseError as err2:
|
||||
# Still HTTP auth errors after refresh → trigger HA reauth
|
||||
if err2.status in (401, 403):
|
||||
raise ConfigEntryAuthFailed from err2
|
||||
raise UpdateFailed(f"HTTP {err2.status}") from err2
|
||||
except ClientError as err:
|
||||
# Network issues (timeouts, DNS, etc.)
|
||||
raise UpdateFailed(f"Network error: {err!r}") from err
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"domain": "rituals_perfume_genie",
|
||||
"name": "Rituals Perfume Genie",
|
||||
"codeowners": ["@milanmeu", "@frenck"],
|
||||
"codeowners": ["@milanmeu", "@frenck", "@quebulm"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rituals_perfume_genie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyrituals"],
|
||||
"requirements": ["pyrituals==0.0.6"]
|
||||
"requirements": ["pyrituals==0.0.7"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -9,6 +10,12 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"description": "Please enter the correct password."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
|
||||
@@ -17,14 +17,24 @@ from roborock import (
|
||||
from roborock.data import UserData
|
||||
from roborock.devices.device import RoborockDevice
|
||||
from roborock.devices.device_manager import UserParams, create_device_manager
|
||||
from roborock.map.map_parser import MapParserConfig
|
||||
|
||||
from homeassistant.const import CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_BASE_URL, CONF_USER_DATA, DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
CONF_BASE_URL,
|
||||
CONF_SHOW_BACKGROUND,
|
||||
CONF_USER_DATA,
|
||||
DEFAULT_DRAWABLES,
|
||||
DOMAIN,
|
||||
DRAWABLES,
|
||||
MAP_SCALE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .coordinator import (
|
||||
RoborockConfigEntry,
|
||||
RoborockCoordinators,
|
||||
@@ -56,6 +66,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
user_params,
|
||||
cache=cache,
|
||||
session=async_get_clientsession(hass),
|
||||
map_parser_config=MapParserConfig(
|
||||
drawables=[
|
||||
drawable
|
||||
for drawable, default_value in DEFAULT_DRAWABLES.items()
|
||||
if entry.options.get(DRAWABLES, {}).get(drawable, default_value)
|
||||
],
|
||||
show_background=entry.options.get(CONF_SHOW_BACKGROUND, False),
|
||||
map_scale=MAP_SCALE,
|
||||
),
|
||||
)
|
||||
except RoborockInvalidCredentials as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -80,10 +99,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="home_data_fail",
|
||||
) from err
|
||||
|
||||
async def shutdown_roborock(_: Event | None = None) -> None:
|
||||
await asyncio.gather(device_manager.close(), cache.flush())
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_roborock)
|
||||
)
|
||||
entry.async_on_unload(shutdown_roborock)
|
||||
|
||||
devices = await device_manager.get_devices()
|
||||
_LOGGER.debug("Device manager found %d devices", len(devices))
|
||||
for device in devices:
|
||||
entry.async_on_unload(device.close)
|
||||
|
||||
coordinators = await asyncio.gather(
|
||||
*build_setup_functions(hass, entry, devices, user_data),
|
||||
@@ -105,25 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_coordinators",
|
||||
)
|
||||
valid_coordinators = RoborockCoordinators(v1_coords, a01_coords)
|
||||
|
||||
async def on_stop(_: Any) -> None:
|
||||
_LOGGER.debug("Shutting down roborock")
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_shutdown()
|
||||
for coordinator in valid_coordinators.values()
|
||||
),
|
||||
cache.flush(),
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
on_stop,
|
||||
)
|
||||
)
|
||||
entry.runtime_data = valid_coordinators
|
||||
entry.runtime_data = RoborockCoordinators(v1_coords, a01_coords)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==3.9.3",
|
||||
"python-roborock==3.10.10",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sharkiq"],
|
||||
"requirements": ["sharkiq==1.4.2"]
|
||||
"requirements": ["sharkiq==1.5.0"]
|
||||
}
|
||||
|
||||
@@ -170,6 +170,9 @@ async def _async_setup_block_entry(
|
||||
device_entry = dev_reg.async_get_device(
|
||||
connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
|
||||
)
|
||||
# https://github.com/home-assistant/core/pull/48076
|
||||
if device_entry and entry.entry_id not in device_entry.config_entries:
|
||||
device_entry = None
|
||||
|
||||
sleep_period = entry.data.get(CONF_SLEEP_PERIOD)
|
||||
runtime_data = entry.runtime_data
|
||||
@@ -280,6 +283,9 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
device_entry = dev_reg.async_get_device(
|
||||
connections={(CONNECTION_NETWORK_MAC, dr.format_mac(entry.unique_id))},
|
||||
)
|
||||
# https://github.com/home-assistant/core/pull/48076
|
||||
if device_entry and entry.entry_id not in device_entry.config_entries:
|
||||
device_entry = None
|
||||
|
||||
sleep_period = entry.data.get(CONF_SLEEP_PERIOD)
|
||||
runtime_data = entry.runtime_data
|
||||
|
||||
@@ -44,6 +44,7 @@ from .entity import (
|
||||
)
|
||||
from .utils import (
|
||||
async_remove_orphaned_entities,
|
||||
async_remove_shelly_entity,
|
||||
format_ble_addr,
|
||||
get_blu_trv_device_info,
|
||||
get_device_entry_gen,
|
||||
@@ -80,6 +81,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_reboot",
|
||||
supported=lambda coordinator: coordinator.sleep_period == 0,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="self_test",
|
||||
@@ -197,7 +199,8 @@ async def async_setup_entry(
|
||||
"""Set up button entities."""
|
||||
entry_data = config_entry.runtime_data
|
||||
coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator | None
|
||||
if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
|
||||
device_gen = get_device_entry_gen(config_entry)
|
||||
if device_gen in RPC_GENERATIONS:
|
||||
coordinator = entry_data.rpc
|
||||
else:
|
||||
coordinator = entry_data.block
|
||||
@@ -210,6 +213,12 @@ async def async_setup_entry(
|
||||
hass, config_entry.entry_id, partial(async_migrate_unique_ids, coordinator)
|
||||
)
|
||||
|
||||
# Remove the 'restart' button for sleeping devices as it was mistakenly
|
||||
# added in https://github.com/home-assistant/core/pull/154673
|
||||
entry_sleep_period = config_entry.data[CONF_SLEEP_PERIOD]
|
||||
if device_gen in RPC_GENERATIONS and entry_sleep_period:
|
||||
async_remove_shelly_entity(hass, BUTTON_PLATFORM, f"{coordinator.mac}-reboot")
|
||||
|
||||
entities: list[ShellyButton] = []
|
||||
|
||||
entities.extend(
|
||||
@@ -224,7 +233,7 @@ async def async_setup_entry(
|
||||
return
|
||||
|
||||
# add RPC buttons
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
if entry_sleep_period:
|
||||
async_setup_entry_rpc(
|
||||
hass,
|
||||
config_entry,
|
||||
|
||||
@@ -72,7 +72,6 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
def _get_starlink_data(self) -> StarlinkData:
|
||||
"""Retrieve Starlink data."""
|
||||
context = self.channel_context
|
||||
status = status_data(context)
|
||||
location = location_data(context)
|
||||
sleep = get_sleep_config(context)
|
||||
status, obstruction, alert = status_data(context)
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import now
|
||||
from homeassistant.util.variance import ignore_variance
|
||||
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
@@ -91,6 +92,10 @@ class StarlinkAccumulationSensor(StarlinkSensorEntity, RestoreSensor):
|
||||
self._attr_native_value = last_native_value
|
||||
|
||||
|
||||
uptime_to_stable_datetime = ignore_variance(
|
||||
lambda value: now() - timedelta(seconds=value), timedelta(minutes=1)
|
||||
)
|
||||
|
||||
SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
|
||||
StarlinkSensorEntityDescription(
|
||||
key="ping",
|
||||
@@ -150,9 +155,7 @@ SENSORS: tuple[StarlinkSensorEntityDescription, ...] = (
|
||||
translation_key="last_restart",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda data: (
|
||||
now() - timedelta(seconds=data.status["uptime"], milliseconds=-500)
|
||||
).replace(microsecond=0),
|
||||
value_fn=lambda data: uptime_to_stable_datetime(data.status["uptime"]),
|
||||
entity_class=StarlinkSensorEntity,
|
||||
),
|
||||
StarlinkSensorEntityDescription(
|
||||
|
||||
@@ -422,6 +422,9 @@ async def async_setup_entry(
|
||||
},
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": "http://robotjs.io/docs/syntax#keys"
|
||||
},
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN, SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
from .const import DATA_WAIT_TIMEOUT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -134,9 +134,6 @@ class SystemBridgeConfigFlow(
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
},
|
||||
)
|
||||
|
||||
errors, info = await _async_get_info(self.hass, user_input)
|
||||
@@ -151,9 +148,6 @@ class SystemBridgeConfigFlow(
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_authenticate(
|
||||
@@ -185,7 +179,6 @@ class SystemBridgeConfigFlow(
|
||||
data_schema=STEP_AUTHENTICATE_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"name": self._name,
|
||||
"syntax_keys_documentation_url": SYNTAX_KEYS_DOCUMENTATION_URL,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -4,8 +4,6 @@ from typing import Final
|
||||
|
||||
from systembridgemodels.modules import Module
|
||||
|
||||
SYNTAX_KEYS_DOCUMENTATION_URL = "http://robotjs.io/docs/syntax#keys"
|
||||
|
||||
DOMAIN = "system_bridge"
|
||||
|
||||
MODULES: Final[list[Module]] = [
|
||||
|
||||
@@ -524,6 +524,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_send_telegram_message,
|
||||
schema=schema,
|
||||
supports_response=supports_response,
|
||||
description_placeholders={
|
||||
"formatting_options_url": "https://core.telegram.org/bots/api#formatting-options"
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -64,6 +64,12 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
|
||||
"botfather_username": "@BotFather",
|
||||
"botfather_url": "https://t.me/botfather",
|
||||
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
|
||||
}
|
||||
|
||||
STEP_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): SelectSelector(
|
||||
@@ -310,10 +316,7 @@ class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow to create a new config entry for a Telegram bot."""
|
||||
|
||||
description_placeholders: dict[str, str] = {
|
||||
"botfather_username": "@BotFather",
|
||||
"botfather_url": "https://t.me/botfather",
|
||||
}
|
||||
description_placeholders: dict[str, str] = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -552,13 +555,14 @@ class TelgramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
},
|
||||
),
|
||||
description_placeholders=DESCRIPTION_PLACEHOLDERS,
|
||||
)
|
||||
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
|
||||
CONF_PROXY_URL
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
|
||||
user_input[CONF_API_KEY] = api_key
|
||||
bot_name = await self._validate_bot(
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"proxy_url": "Proxy URL"
|
||||
},
|
||||
"data_description": {
|
||||
"proxy_url": "Proxy URL if working behind one, optionally including username and password.\n(socks5://username:password@proxy_ip:proxy_port)"
|
||||
"proxy_url": "Proxy URL if working behind one, optionally including username and password.\n({socks_url})"
|
||||
},
|
||||
"name": "Advanced settings"
|
||||
}
|
||||
@@ -400,7 +400,7 @@
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the media.",
|
||||
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
|
||||
},
|
||||
"chat_id": {
|
||||
@@ -499,7 +499,7 @@
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the animation.",
|
||||
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
|
||||
},
|
||||
"config_entry_id": {
|
||||
@@ -600,7 +600,7 @@
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the document.",
|
||||
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
|
||||
},
|
||||
"config_entry_id": {
|
||||
@@ -745,7 +745,7 @@
|
||||
"name": "Keyboard"
|
||||
},
|
||||
"message": {
|
||||
"description": "Message body of the notification.",
|
||||
"description": "Message body of the notification.\nCan't parse entities? Format your message according to the [formatting options]({formatting_options_url}).",
|
||||
"name": "Message"
|
||||
},
|
||||
"message_tag": {
|
||||
@@ -757,7 +757,7 @@
|
||||
"name": "Message thread ID"
|
||||
},
|
||||
"parse_mode": {
|
||||
"description": "Parser for the message text.",
|
||||
"description": "Parser for the message text.\nSee [formatting options]({formatting_options_url}) for more details.",
|
||||
"name": "Parse mode"
|
||||
},
|
||||
"reply_to_message_id": {
|
||||
@@ -787,7 +787,7 @@
|
||||
"name": "Authentication method"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the image.",
|
||||
"description": "The title of the media.\nCan't parse entities? Format your message according to the [formatting options]({formatting_options_url}).",
|
||||
"name": "Caption"
|
||||
},
|
||||
"config_entry_id": {
|
||||
@@ -991,7 +991,7 @@
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the video.",
|
||||
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
|
||||
},
|
||||
"config_entry_id": {
|
||||
@@ -1070,7 +1070,7 @@
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::authentication::name%]"
|
||||
},
|
||||
"caption": {
|
||||
"description": "The title of the voice message.",
|
||||
"description": "[%key:component::telegram_bot::services::send_photo::fields::caption::description%]",
|
||||
"name": "[%key:component::telegram_bot::services::send_photo::fields::caption::name%]"
|
||||
},
|
||||
"config_entry_id": {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -346,12 +347,21 @@ async def async_validate_config_section(
|
||||
|
||||
async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType:
|
||||
"""Validate config."""
|
||||
if DOMAIN not in config:
|
||||
|
||||
configs = []
|
||||
for key in config:
|
||||
if DOMAIN not in key:
|
||||
continue
|
||||
|
||||
if key == DOMAIN or (key.startswith(DOMAIN) and len(key.split()) > 1):
|
||||
configs.append(cv.ensure_list(config[key]))
|
||||
|
||||
if not configs:
|
||||
return config
|
||||
|
||||
config_sections = []
|
||||
|
||||
for cfg in cv.ensure_list(config[DOMAIN]):
|
||||
for cfg in itertools.chain(*configs):
|
||||
try:
|
||||
template_config: TemplateConfig = await async_validate_config_section(
|
||||
hass, cfg
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Helpers for template integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
import hashlib
|
||||
import itertools
|
||||
import logging
|
||||
@@ -12,6 +12,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITY_PICTURE_TEMPLATE,
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_ICON,
|
||||
@@ -33,6 +34,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import yaml as yaml_util
|
||||
@@ -132,6 +134,9 @@ def rewrite_legacy_to_modern_config(
|
||||
"""Rewrite legacy config."""
|
||||
entity_cfg = {**entity_cfg}
|
||||
|
||||
# Remove deprecated entity_id field from legacy syntax
|
||||
entity_cfg.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
for from_key, to_key in itertools.chain(
|
||||
LEGACY_FIELDS.items(), extra_legacy_fields.items()
|
||||
):
|
||||
@@ -190,12 +195,12 @@ def async_create_template_tracking_entities(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
def _format_template(value: Any) -> Any:
|
||||
def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
if isinstance(value, template.Template):
|
||||
return value.template
|
||||
|
||||
if isinstance(value, Enum):
|
||||
return value.name
|
||||
if isinstance(value, StrEnum):
|
||||
return value.value
|
||||
|
||||
if isinstance(value, (int, float, str, bool)):
|
||||
return value
|
||||
@@ -207,14 +212,13 @@ def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
"""Recursive method to format templates as strings from ConfigType."""
|
||||
types = (dict, list)
|
||||
if depth > 9:
|
||||
raise RecursionError
|
||||
|
||||
if isinstance(config, list):
|
||||
items = []
|
||||
for item in config:
|
||||
if isinstance(item, types):
|
||||
if isinstance(item, (dict, list)):
|
||||
if len(item) > 0:
|
||||
items.append(format_migration_config(item, depth + 1))
|
||||
else:
|
||||
@@ -223,9 +227,18 @@ def format_migration_config(
|
||||
|
||||
formatted_config = {}
|
||||
for field, value in config.items():
|
||||
if isinstance(value, types):
|
||||
if isinstance(value, dict):
|
||||
if len(value) > 0:
|
||||
formatted_config[field] = format_migration_config(value, depth + 1)
|
||||
elif isinstance(value, list):
|
||||
if len(value) > 0:
|
||||
formatted_config[field] = format_migration_config(value, depth + 1)
|
||||
else:
|
||||
formatted_config[field] = []
|
||||
elif isinstance(value, ScriptVariables):
|
||||
formatted_config[field] = format_migration_config(
|
||||
value.as_dict(), depth + 1
|
||||
)
|
||||
else:
|
||||
formatted_config[field] = _format_template(value)
|
||||
|
||||
@@ -260,9 +273,9 @@ def create_legacy_template_issue(
|
||||
try:
|
||||
config.pop(CONF_PLATFORM, None)
|
||||
modified_yaml = format_migration_config(config)
|
||||
yaml_config = yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})
|
||||
# Format to show up properly in a numbered bullet on the repair.
|
||||
yaml_config = " ```\n " + yaml_config.replace("\n", "\n ") + "```"
|
||||
yaml_config = (
|
||||
f"```\n{yaml_util.dump({DOMAIN: [{domain: [modified_yaml]}]})}\n```"
|
||||
)
|
||||
except RecursionError:
|
||||
yaml_config = f"{DOMAIN}:\n - {domain}: - ..."
|
||||
|
||||
@@ -278,6 +291,7 @@ def create_legacy_template_issue(
|
||||
"domain": domain,
|
||||
"breadcrumb": breadcrumb,
|
||||
"config": yaml_config,
|
||||
"filename": "<filename>",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -635,14 +635,14 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
# Support legacy mireds in template light.
|
||||
temperature = int(render)
|
||||
if (min_kelvin := self._attr_min_color_temp_kelvin) is not None:
|
||||
min_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
|
||||
else:
|
||||
min_mireds = DEFAULT_MIN_MIREDS
|
||||
|
||||
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
|
||||
max_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
|
||||
max_mireds = color_util.color_temperature_kelvin_to_mired(min_kelvin)
|
||||
else:
|
||||
max_mireds = DEFAULT_MAX_MIREDS
|
||||
|
||||
if (max_kelvin := self._attr_max_color_temp_kelvin) is not None:
|
||||
min_mireds = color_util.color_temperature_kelvin_to_mired(max_kelvin)
|
||||
else:
|
||||
min_mireds = DEFAULT_MIN_MIREDS
|
||||
if min_mireds <= temperature <= max_mireds:
|
||||
self._attr_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(temperature)
|
||||
@@ -856,42 +856,36 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
|
||||
try:
|
||||
if render in (None, "None", ""):
|
||||
self._attr_max_mireds = DEFAULT_MAX_MIREDS
|
||||
self._attr_max_color_temp_kelvin = None
|
||||
self._attr_min_color_temp_kelvin = None
|
||||
return
|
||||
|
||||
self._attr_max_mireds = max_mireds = int(render)
|
||||
self._attr_max_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(max_mireds)
|
||||
self._attr_min_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(int(render))
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.exception(
|
||||
"Template must supply an integer temperature within the range for"
|
||||
" this light, or 'None'"
|
||||
)
|
||||
self._attr_max_mireds = DEFAULT_MAX_MIREDS
|
||||
self._attr_max_color_temp_kelvin = None
|
||||
self._attr_min_color_temp_kelvin = None
|
||||
|
||||
@callback
|
||||
def _update_min_mireds(self, render):
|
||||
"""Update the min mireds from the template."""
|
||||
try:
|
||||
if render in (None, "None", ""):
|
||||
self._attr_min_mireds = DEFAULT_MIN_MIREDS
|
||||
self._attr_min_color_temp_kelvin = None
|
||||
self._attr_max_color_temp_kelvin = None
|
||||
return
|
||||
|
||||
self._attr_min_mireds = min_mireds = int(render)
|
||||
self._attr_min_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(min_mireds)
|
||||
self._attr_max_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(int(render))
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.exception(
|
||||
"Template must supply an integer temperature within the range for"
|
||||
" this light, or 'None'"
|
||||
)
|
||||
self._attr_min_mireds = DEFAULT_MIN_MIREDS
|
||||
self._attr_min_color_temp_kelvin = None
|
||||
self._attr_max_color_temp_kelvin = None
|
||||
|
||||
@callback
|
||||
def _update_supports_transition(self, render):
|
||||
|
||||
@@ -529,7 +529,7 @@
|
||||
"title": "Deprecated battery level option in {entity_name}"
|
||||
},
|
||||
"deprecated_legacy_templates": {
|
||||
"description": "The legacy `platform: template` syntax for `{domain}` is being removed. Please migrate `{breadcrumb}` to the modern template syntax.\n\n1. Remove existing template definition.\n2. Add new template definition:\n{config}\n3. Restart Home Assistant or reload template entities.",
|
||||
"description": "The legacy `platform: template` syntax for `{domain}` is being removed. Please migrate `{breadcrumb}` to the modern template syntax.\n#### Step 1 - Remove legacy configuration\nRemove the `{breadcrumb}` template definition from the `configuration.yaml` `{domain}:` section.\n\n**Note:** If you are using `{domain}: !include {filename}.yaml` in `configuration.yaml`, remove the {domain} definition from the included `{filename}.yaml`.\n#### Step 2 - Add the modern configuration\nAdd new template definition inside `configuration.yaml`:\n{config}\n**Note:** If there are any existing `template:` sections in your configuration, make sure to omit the `template:` line from the yaml above. There can only be 1 `template:` section in `configuration.yaml`. Also, ensure the indentation is aligned with the existing entities within the `template:` section.\n#### Step 3 - Restart Home Assistant or reload template entities",
|
||||
"title": "Legacy {domain} template deprecation"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -325,6 +325,9 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
vol.Required(ATTR_TOU_SETTINGS): dict,
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"time_of_use_url": "https://developer.tesla.com/docs/fleet-api#time_of_use_settings"
|
||||
},
|
||||
)
|
||||
|
||||
async def add_charge_schedule(call: ServiceCall) -> None:
|
||||
|
||||
@@ -1358,7 +1358,7 @@
|
||||
"name": "Energy Site"
|
||||
},
|
||||
"tou_settings": {
|
||||
"description": "See https://developer.tesla.com/docs/fleet-api#time_of_use_settings for details.",
|
||||
"description": "See {time_of_use_url} for details.",
|
||||
"name": "Settings"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -125,6 +125,18 @@ def _get_temperature_wrappers(
|
||||
device, DPCode.TEMP_SET_F, prefer_function=True
|
||||
)
|
||||
|
||||
# If there is a temp unit convert dpcode, override empty units
|
||||
if (
|
||||
temp_unit_convert := DPCodeEnumWrapper.find_dpcode(
|
||||
device, DPCode.TEMP_UNIT_CONVERT
|
||||
)
|
||||
) is not None:
|
||||
for wrapper in (temp_current, temp_current_f, temp_set, temp_set_f):
|
||||
if wrapper is not None and not wrapper.type_information.unit:
|
||||
wrapper.type_information.unit = temp_unit_convert.read_device_status(
|
||||
device
|
||||
)
|
||||
|
||||
# Get wrappers for celsius and fahrenheit
|
||||
# We need to check the unit of measurement
|
||||
current_celsius = _get_temperature_wrapper(
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.31.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.33.2", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -66,7 +66,7 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def discover(devices):
|
||||
"""Add new devices to platform."""
|
||||
_setup_entities(devices, async_add_entities)
|
||||
_setup_entities(devices, async_add_entities, coordinator)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_DEVICES), discover)
|
||||
|
||||
@@ -19,6 +19,8 @@ from pythonxbox.api.provider.smartglass.models import (
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||
|
||||
from .entity import to_https
|
||||
|
||||
|
||||
class MediaTypeDetails(NamedTuple):
|
||||
"""Details for media type."""
|
||||
@@ -151,5 +153,5 @@ def _find_media_image(images: list[Image]) -> str | None:
|
||||
if match := next(
|
||||
(image for image in images if image.image_purpose == purpose), None
|
||||
):
|
||||
return f"https:{match.uri}" if match.uri.startswith("/") else match.uri
|
||||
return to_https(match.uri)
|
||||
return None
|
||||
|
||||
@@ -151,6 +151,15 @@ def check_deprecated_entity(
|
||||
return False
|
||||
|
||||
|
||||
def to_https(image_url: str) -> str:
|
||||
"""Convert image URLs to secure URLs."""
|
||||
|
||||
url = URL(image_url)
|
||||
if url.host == "images-eds.xboxlive.com":
|
||||
url = url.with_host("images-eds-ssl.xboxlive.com")
|
||||
return str(url.with_scheme("https"))
|
||||
|
||||
|
||||
def profile_pic(person: Person, _: Title | None = None) -> str | None:
|
||||
"""Return the gamer pic."""
|
||||
|
||||
@@ -160,9 +169,4 @@ def profile_pic(person: Person, _: Title | None = None) -> str | None:
|
||||
# to point to the correct image, with the correct domain and certificate.
|
||||
# We need to also remove the 'mode=Padding' query because with it,
|
||||
# it results in an error 400.
|
||||
url = URL(person.display_pic_raw)
|
||||
if url.host == "images-eds.xboxlive.com":
|
||||
url = url.with_host("images-eds-ssl.xboxlive.com").with_scheme("https")
|
||||
query = dict(url.query)
|
||||
query.pop("mode", None)
|
||||
return str(url.with_query(query))
|
||||
return str(URL(to_https(person.display_pic_raw)).without_query_params("mode"))
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.util import dt as dt_util
|
||||
from .binary_sensor import profile_pic
|
||||
from .const import DOMAIN
|
||||
from .coordinator import XboxConfigEntry
|
||||
from .entity import to_https
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -629,7 +630,7 @@ class XboxSource(MediaSource):
|
||||
title=image.type,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=image.url,
|
||||
thumbnail=to_https(image.url),
|
||||
)
|
||||
for image in game.images
|
||||
]
|
||||
@@ -655,6 +656,6 @@ def game_thumbnail(images: list[Image]) -> str | None:
|
||||
(i for i in images if i.type == img_type),
|
||||
None,
|
||||
):
|
||||
return match.url
|
||||
return to_https(match.url)
|
||||
|
||||
return None
|
||||
|
||||
@@ -34,6 +34,7 @@ from .entity import (
|
||||
XboxBaseEntity,
|
||||
XboxBaseEntityDescription,
|
||||
check_deprecated_entity,
|
||||
to_https,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -142,8 +143,8 @@ def title_logo(_: Person, title: Title | None) -> str | None:
|
||||
"""Get the game logo."""
|
||||
|
||||
return (
|
||||
next((i.url for i in title.images if i.type == "Tile"), None)
|
||||
or next((i.url for i in title.images if i.type == "Logo"), None)
|
||||
next((to_https(i.url) for i in title.images if i.type == "Tile"), None)
|
||||
or next((to_https(i.url) for i in title.images if i.type == "Logo"), None)
|
||||
if title and title.images
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -44,6 +44,11 @@ GATEWAY_SETTINGS = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ERROR_STEP_PLACEHOLDERS = {
|
||||
"tutorial_url": "https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz",
|
||||
"invalid_host_url": "https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
|
||||
}
|
||||
|
||||
|
||||
class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Xiaomi Aqara config flow."""
|
||||
@@ -66,7 +71,12 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if (self.host is None and self.sid is None) or errors:
|
||||
schema = GATEWAY_CONFIG_HOST
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=schema,
|
||||
errors=errors,
|
||||
description_placeholders=ERROR_STEP_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -149,7 +159,10 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="select", data_schema=select_schema, errors=errors
|
||||
step_id="select",
|
||||
data_schema=select_schema,
|
||||
errors=errors,
|
||||
description_placeholders=ERROR_STEP_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
@@ -236,5 +249,8 @@ class XiaomiAqaraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors[CONF_KEY] = "invalid_key"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="settings", data_schema=GATEWAY_SETTINGS, errors=errors
|
||||
step_id="settings",
|
||||
data_schema=GATEWAY_SETTINGS,
|
||||
errors=errors,
|
||||
description_placeholders=ERROR_STEP_PLACEHOLDERS,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"error": {
|
||||
"discovery_error": "Failed to discover a Xiaomi Aqara Gateway, try using the IP of the device running Home Assistant as interface",
|
||||
"invalid_host": "Invalid hostname or IP address, see https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem",
|
||||
"invalid_host": "Invalid hostname or IP address, see {invalid_host_url}",
|
||||
"invalid_interface": "Invalid network interface",
|
||||
"invalid_key": "Invalid Gateway key",
|
||||
"invalid_mac": "Invalid MAC address"
|
||||
@@ -25,7 +25,7 @@
|
||||
"key": "The key of your Gateway",
|
||||
"name": "Name of the Gateway"
|
||||
},
|
||||
"description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible",
|
||||
"description": "The key (password) can be retrieved using this tutorial: {tutorial_url}. If the key is not provided only sensors will be accessible",
|
||||
"title": "Optional settings"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -63,6 +63,7 @@ from .device import YeelightDevice
|
||||
from .entity import YeelightEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_EXAMPLES_URL = "https://yeelight.readthedocs.io/en/stable/flow.html"
|
||||
|
||||
ATTR_MINUTES = "minutes"
|
||||
ATTR_KELVIN = "kelvin"
|
||||
@@ -380,7 +381,13 @@ def _async_setup_services(hass: HomeAssistant):
|
||||
SERVICE_SET_MODE, SERVICE_SCHEMA_SET_MODE, "async_set_mode"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_START_FLOW, SERVICE_SCHEMA_START_FLOW, _async_start_flow
|
||||
SERVICE_START_FLOW,
|
||||
SERVICE_SCHEMA_START_FLOW,
|
||||
_async_start_flow,
|
||||
description_placeholders={
|
||||
"examples_url": _EXAMPLES_URL,
|
||||
"flow_objects_urls": "https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects",
|
||||
},
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_COLOR_SCENE, SERVICE_SCHEMA_SET_COLOR_SCENE, _async_set_color_scene
|
||||
@@ -397,6 +404,7 @@ def _async_setup_services(hass: HomeAssistant):
|
||||
SERVICE_SET_COLOR_FLOW_SCENE,
|
||||
SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE,
|
||||
_async_set_color_flow_scene,
|
||||
description_placeholders={"examples_url": _EXAMPLES_URL},
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_AUTO_DELAY_OFF_SCENE,
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
"name": "Count"
|
||||
},
|
||||
"transitions": {
|
||||
"description": "Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html.",
|
||||
"description": "Array of transitions, for desired effect. Examples {examples_url}.",
|
||||
"name": "Transitions"
|
||||
}
|
||||
},
|
||||
@@ -171,7 +171,7 @@
|
||||
"name": "Set music mode"
|
||||
},
|
||||
"start_flow": {
|
||||
"description": "Starts a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects.",
|
||||
"description": "Starts a custom flow, using transitions from {flow_objects_urls}.",
|
||||
"fields": {
|
||||
"action": {
|
||||
"description": "[%key:component::yeelight::services::set_color_flow_scene::fields::action::description%]",
|
||||
|
||||
@@ -680,6 +680,13 @@ class BaseZhaFlow(ConfigEntryBaseFlow):
|
||||
|
||||
try:
|
||||
await self._form_network_task
|
||||
except Exception as exc:
|
||||
_LOGGER.exception("Failed to form new network")
|
||||
self._progress_error = AbortFlow(
|
||||
reason="cannot_form_network",
|
||||
description_placeholders={"error": str(exc)},
|
||||
)
|
||||
return self.async_show_progress_done(next_step_id="progress_failed")
|
||||
finally:
|
||||
self._form_network_task = None
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"cannot_form_network": "Could not form a new Zigbee network.\n\nError: {error}",
|
||||
"cannot_resolve_path": "Could not resolve device path: {path}",
|
||||
"cannot_restore_backup": "The adapter you are restoring to does not properly support backup restoration. Please upgrade the firmware.\n\nError: {error}",
|
||||
"cannot_restore_backup_no_ieee_confirm": "The adapter you are restoring to has outdated firmware and cannot write the adapter IEEE address multiple times. Please upgrade the firmware or confirm permanent overwrite in the previous step.",
|
||||
@@ -1913,16 +1914,17 @@
|
||||
"title": "Zigbee network settings have changed"
|
||||
},
|
||||
"wrong_silabs_firmware_installed_nabucasa": {
|
||||
"description": "Your Zigbee adapter was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}).\n\nTo run your adapter exclusively with ZHA, you need to install the Zigbee firmware:\n - Go to Settings > System > Hardware, select the device and select Configure.\n - Select the 'Migrate Zigbee to a new adapter' option and follow the instructions.",
|
||||
"title": "Zigbee adapter with multiprotocol firmware detected"
|
||||
"description": "Your Zigbee adapter is currently in an incorrect state: {firmware_type}.\n\nThe device may have Thread or multiprotocol firmware installed, or it may be stuck in the bootloader. To resolve this, try to unplug the adapter temporarily.\n\nIf the issue persists and you need to install Zigbee firmware:\n - Go to Settings > System > Hardware, select the device and select Configure.\n - Select the 'Migrate Zigbee to a new adapter' option and follow the instructions.",
|
||||
"title": "Zigbee adapter in incorrect state"
|
||||
},
|
||||
"wrong_silabs_firmware_installed_other": {
|
||||
"description": "Your Zigbee adapter was previously used with multiprotocol (Zigbee and Thread) and still has multiprotocol firmware installed: ({firmware_type}).\n\nTo run your adapter exclusively with ZHA, you need to install Zigbee firmware. Follow your Zigbee adapter manufacturer's instructions for how to do this.",
|
||||
"description": "Your Zigbee adapter is currently in an incorrect state: {firmware_type}.\n\nThe device may have Thread or multiprotocol firmware installed, or it may be stuck in the bootloader. To resolve this, try to unplug the adapter temporarily.\n\nIf the issue persists and you need to install Zigbee firmware, follow your Zigbee adapter manufacturer's instructions for how to do this.",
|
||||
"title": "[%key:component::zha::issues::wrong_silabs_firmware_installed_nabucasa::title%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"cannot_form_network": "[%key:component::zha::config::abort::cannot_form_network%]",
|
||||
"cannot_resolve_path": "[%key:component::zha::config::abort::cannot_resolve_path%]",
|
||||
"cannot_restore_backup": "[%key:component::zha::config::abort::cannot_restore_backup%]",
|
||||
"cannot_restore_backup_no_ieee_confirm": "[%key:component::zha::config::abort::cannot_restore_backup_no_ieee_confirm%]",
|
||||
|
||||
@@ -452,6 +452,9 @@ class ZWaveServices:
|
||||
has_at_least_one_node,
|
||||
),
|
||||
),
|
||||
description_placeholders={
|
||||
"api_docs_url": "https://zwave-js.github.io/node-zwave-js/#/api/CCs/index"
|
||||
},
|
||||
)
|
||||
|
||||
self._hass.services.async_register(
|
||||
|
||||
@@ -400,11 +400,11 @@
|
||||
"name": "Entity ID(s)"
|
||||
},
|
||||
"method_name": {
|
||||
"description": "The name of the API method to call. Refer to the Z-Wave Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for available methods.",
|
||||
"description": "The name of the API method to call. Refer to the Z-Wave Command Class API documentation ({api_docs_url}) for available methods.",
|
||||
"name": "Method name"
|
||||
},
|
||||
"parameters": {
|
||||
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave Command Class API documentation (https://zwave-js.github.io/node-zwave-js/#/api/CCs/index) for parameters.",
|
||||
"description": "A list of parameters to pass to the API method. Refer to the Z-Wave Command Class API documentation ({api_docs_url}) for parameters.",
|
||||
"name": "Parameters"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 12
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
||||
@@ -124,6 +124,12 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
||||
# Added in 2025.10.0 because of
|
||||
# https://github.com/frenck/spook/issues/1066
|
||||
"spook": BlockedIntegration(AwesomeVersion("4.0.0"), "breaks the template engine"),
|
||||
# Added in 2025.12.1 because of
|
||||
# https://github.com/JaccoR/hass-entso-e/issues/263
|
||||
"entsoe": BlockedIntegration(
|
||||
AwesomeVersion("0.7.1"),
|
||||
"crashes Home Assistant when it can't connect to the API",
|
||||
),
|
||||
}
|
||||
|
||||
DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey(
|
||||
|
||||
@@ -39,7 +39,7 @@ habluetooth==5.7.0
|
||||
hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20251203.0
|
||||
home-assistant-frontend==20251203.2
|
||||
home-assistant-intents==2025.12.2
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.12.0"
|
||||
version = "2025.12.2"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
26
requirements_all.txt
generated
26
requirements_all.txt
generated
@@ -22,7 +22,7 @@ HAP-python==5.0.0
|
||||
HATasmota==0.10.1
|
||||
|
||||
# homeassistant.components.hue_ble
|
||||
HueBLE==1.0.8
|
||||
HueBLE==2.1.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
@@ -537,7 +537,7 @@ arris-tg2492lg==2.2.0
|
||||
asmog==0.0.6
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
asusrouter==1.21.0
|
||||
asusrouter==1.21.1
|
||||
|
||||
# homeassistant.components.dlna_dmr
|
||||
# homeassistant.components.dlna_dms
|
||||
@@ -935,7 +935,7 @@ eufylife-ble-client==0.1.8
|
||||
# evdev==1.6.1
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.5
|
||||
evohome-async==1.0.6
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@@ -1090,7 +1090,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==1.1.3
|
||||
google_air_quality_api==2.0.0
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -1198,7 +1198,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251203.0
|
||||
home-assistant-frontend==20251203.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.12.2
|
||||
@@ -1669,7 +1669,7 @@ openwrt-ubus-rpc==0.0.2
|
||||
opower==0.15.9
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
oralb-ble==1.0.2
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@@ -2187,7 +2187,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.6.0
|
||||
pymiele==0.6.1
|
||||
|
||||
# homeassistant.components.xiaomi_tv
|
||||
pymitv==1.4.3
|
||||
@@ -2342,7 +2342,7 @@ pyrepetierng==0.1.0
|
||||
pyrisco==0.6.7
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
pyrituals==0.0.7
|
||||
|
||||
# homeassistant.components.thread
|
||||
pyroute2==0.7.5
|
||||
@@ -2557,7 +2557,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==3.9.3
|
||||
python-roborock==3.10.10
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.45
|
||||
@@ -2717,7 +2717,7 @@ renault-api==0.5.1
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.17.0
|
||||
reolink-aio==0.17.1
|
||||
|
||||
# homeassistant.components.idteck_prox
|
||||
rfk101py==0.0.1
|
||||
@@ -2820,7 +2820,7 @@ sentry-sdk==1.45.1
|
||||
sfrbox-api==0.1.0
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.4.2
|
||||
sharkiq==1.5.0
|
||||
|
||||
# homeassistant.components.aquostv
|
||||
sharp_aquos_rc==0.3.2
|
||||
@@ -3050,7 +3050,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.31.0
|
||||
uiprotect==7.33.2
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -3228,7 +3228,7 @@ youless-api==2.2.0
|
||||
youtubeaio==2.1.1
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2025.11.12
|
||||
yt-dlp[default]==2025.12.08
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
zabbix-utils==2.0.3
|
||||
|
||||
26
requirements_test_all.txt
generated
26
requirements_test_all.txt
generated
@@ -22,7 +22,7 @@ HAP-python==5.0.0
|
||||
HATasmota==0.10.1
|
||||
|
||||
# homeassistant.components.hue_ble
|
||||
HueBLE==1.0.8
|
||||
HueBLE==2.1.0
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
@@ -504,7 +504,7 @@ aranet4==2.5.1
|
||||
arcam-fmj==1.8.2
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
asusrouter==1.21.0
|
||||
asusrouter==1.21.1
|
||||
|
||||
# homeassistant.components.dlna_dmr
|
||||
# homeassistant.components.dlna_dms
|
||||
@@ -823,7 +823,7 @@ eternalegypt==0.0.16
|
||||
eufylife-ble-client==0.1.8
|
||||
|
||||
# homeassistant.components.evohome
|
||||
evohome-async==1.0.5
|
||||
evohome-async==1.0.6
|
||||
|
||||
# homeassistant.components.bryant_evolution
|
||||
evolutionhttp==0.0.18
|
||||
@@ -966,7 +966,7 @@ google-nest-sdm==9.1.2
|
||||
google-photos-library-api==0.12.1
|
||||
|
||||
# homeassistant.components.google_air_quality
|
||||
google_air_quality_api==1.1.3
|
||||
google_air_quality_api==2.0.0
|
||||
|
||||
# homeassistant.components.slide
|
||||
# homeassistant.components.slide_local
|
||||
@@ -1056,7 +1056,7 @@ hole==0.9.0
|
||||
holidays==0.84
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20251203.0
|
||||
home-assistant-frontend==20251203.2
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.12.2
|
||||
@@ -1437,7 +1437,7 @@ openwebifpy==4.3.1
|
||||
opower==0.15.9
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.17.6
|
||||
oralb-ble==1.0.2
|
||||
|
||||
# homeassistant.components.ourgroceries
|
||||
ourgroceries==1.5.4
|
||||
@@ -1840,7 +1840,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.6.0
|
||||
pymiele==0.6.1
|
||||
|
||||
# homeassistant.components.mochad
|
||||
pymochad==0.2.0
|
||||
@@ -1968,7 +1968,7 @@ pyrate-limiter==3.9.0
|
||||
pyrisco==0.6.7
|
||||
|
||||
# homeassistant.components.rituals_perfume_genie
|
||||
pyrituals==0.0.6
|
||||
pyrituals==0.0.7
|
||||
|
||||
# homeassistant.components.thread
|
||||
pyroute2==0.7.5
|
||||
@@ -2135,7 +2135,7 @@ python-pooldose==0.7.8
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==3.9.3
|
||||
python-roborock==3.10.10
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.45
|
||||
@@ -2271,7 +2271,7 @@ renault-api==0.5.1
|
||||
renson-endura-delta==1.7.2
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.17.0
|
||||
reolink-aio==0.17.1
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.67
|
||||
@@ -2353,7 +2353,7 @@ sentry-sdk==1.45.1
|
||||
sfrbox-api==0.1.0
|
||||
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.4.2
|
||||
sharkiq==1.5.0
|
||||
|
||||
# homeassistant.components.simplefin
|
||||
simplefin4py==0.0.18
|
||||
@@ -2535,7 +2535,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.31.0
|
||||
uiprotect==7.33.2
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -2686,7 +2686,7 @@ youless-api==2.2.0
|
||||
youtubeaio==2.1.1
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2025.11.12
|
||||
yt-dlp[default]==2025.12.08
|
||||
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
@@ -40,6 +40,7 @@ async def test_full_flow(
|
||||
user_input={
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_ACCOUNT_NUMBER: ACCOUNT_NUMBER,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -74,6 +75,7 @@ async def test_already_configured(
|
||||
user_input={
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_ACCOUNT_NUMBER: ACCOUNT_NUMBER,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -107,6 +109,7 @@ async def test_auth_recover_exception(
|
||||
user_input={
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_ACCOUNT_NUMBER: ACCOUNT_NUMBER,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -123,6 +126,7 @@ async def test_auth_recover_exception(
|
||||
user_input={
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_ACCOUNT_NUMBER: ACCOUNT_NUMBER,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -164,6 +168,7 @@ async def test_account_recover_exception(
|
||||
user_input={
|
||||
CONF_USERNAME: USERNAME,
|
||||
CONF_PASSWORD: PASSWORD,
|
||||
CONF_ACCOUNT_NUMBER: ACCOUNT_NUMBER,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -82,6 +82,10 @@ def patch_doorbird_api_entry_points(api: MagicMock) -> Generator[DoorBird]:
|
||||
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||
return_value=api,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.doorbird.device.get_url",
|
||||
return_value="http://127.0.0.1:8123",
|
||||
),
|
||||
):
|
||||
yield api
|
||||
|
||||
|
||||
@@ -2,15 +2,141 @@
|
||||
|
||||
from copy import deepcopy
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from doorbirdpy import DoorBirdScheduleEntry
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.doorbird.const import CONF_EVENTS
|
||||
from homeassistant.components.doorbird.const import (
|
||||
CONF_EVENTS,
|
||||
DEFAULT_DOORBELL_EVENT,
|
||||
DEFAULT_MOTION_EVENT,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import VALID_CONFIG
|
||||
from .conftest import DoorbirdMockerType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def doorbird_favorites_with_stale() -> dict[str, dict[str, Any]]:
|
||||
"""Return favorites fixture with stale favorites from another HA instance.
|
||||
|
||||
Creates favorites where identifier "2" has the same event name as "0"
|
||||
(mydoorbird_doorbell) but points to a different HA instance URL.
|
||||
These stale favorites should be filtered out.
|
||||
"""
|
||||
return {
|
||||
"http": {
|
||||
"0": {
|
||||
"title": "Home Assistant (mydoorbird_doorbell)",
|
||||
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_doorbell?token=test-token",
|
||||
},
|
||||
# Stale favorite from a different HA instance - should be filtered out
|
||||
"2": {
|
||||
"title": "Home Assistant (mydoorbird_doorbell)",
|
||||
"value": "http://old-ha-instance:8123/api/doorbird/mydoorbird_doorbell?token=old-token",
|
||||
},
|
||||
"5": {
|
||||
"title": "Home Assistant (mydoorbird_motion)",
|
||||
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=test-token",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def doorbird_schedule_with_stale() -> list[DoorBirdScheduleEntry]:
|
||||
"""Return schedule fixture with outputs referencing stale favorites.
|
||||
|
||||
Both param "0" and "2" map to doorbell input, but "2" is a stale favorite.
|
||||
"""
|
||||
schedule_data = [
|
||||
{
|
||||
"input": "doorbell",
|
||||
"param": "1",
|
||||
"output": [
|
||||
{
|
||||
"event": "http",
|
||||
"param": "0",
|
||||
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
|
||||
},
|
||||
{
|
||||
"event": "http",
|
||||
"param": "2",
|
||||
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"input": "motion",
|
||||
"param": "",
|
||||
"output": [
|
||||
{
|
||||
"event": "http",
|
||||
"param": "5",
|
||||
"schedule": {"weekdays": [{"to": "107999", "from": "108000"}]},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return DoorBirdScheduleEntry.parse_all(schedule_data)
|
||||
|
||||
|
||||
async def test_stale_favorites_filtered_by_url(
|
||||
hass: HomeAssistant,
|
||||
doorbird_mocker: DoorbirdMockerType,
|
||||
doorbird_favorites_with_stale: dict[str, dict[str, Any]],
|
||||
doorbird_schedule_with_stale: list[DoorBirdScheduleEntry],
|
||||
) -> None:
|
||||
"""Test that stale favorites from other HA instances are filtered out."""
|
||||
await doorbird_mocker(
|
||||
favorites=doorbird_favorites_with_stale,
|
||||
schedule=doorbird_schedule_with_stale,
|
||||
)
|
||||
# Should have 2 event entities - stale favorite "2" is filtered out
|
||||
# because its URL doesn't match the current HA instance
|
||||
event_entities = hass.states.async_all("event")
|
||||
assert len(event_entities) == 2
|
||||
|
||||
|
||||
async def test_custom_url_used_for_favorites(
|
||||
hass: HomeAssistant,
|
||||
doorbird_mocker: DoorbirdMockerType,
|
||||
) -> None:
|
||||
"""Test that custom URL override is used instead of get_url."""
|
||||
custom_url = "https://my-custom-url.example.com:8443"
|
||||
favorites = {
|
||||
"http": {
|
||||
"1": {
|
||||
"title": "Home Assistant (mydoorbird_doorbell)",
|
||||
"value": f"{custom_url}/api/doorbird/mydoorbird_doorbell?token=test-token",
|
||||
},
|
||||
"2": {
|
||||
"title": "Home Assistant (mydoorbird_motion)",
|
||||
"value": f"{custom_url}/api/doorbird/mydoorbird_motion?token=test-token",
|
||||
},
|
||||
}
|
||||
}
|
||||
config_with_custom_url = {
|
||||
**VALID_CONFIG,
|
||||
"hass_url_override": custom_url,
|
||||
}
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="1CCAE3AAAAAA",
|
||||
data=config_with_custom_url,
|
||||
options={CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]},
|
||||
)
|
||||
await doorbird_mocker(entry=entry, favorites=favorites)
|
||||
|
||||
# Should have 2 event entities using the custom URL
|
||||
event_entities = hass.states.async_all("event")
|
||||
assert len(event_entities) == 2
|
||||
|
||||
|
||||
async def test_no_configured_events(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -16,6 +16,7 @@ TEST_INSTALLS: Final = (
|
||||
"h032585", # VisionProWifi: no preset modes for TCS, zoneId=systemId
|
||||
"h099625", # RoundThermostat
|
||||
"h139906", # zone with null schedule
|
||||
"h157546", # tcs with long 8-digit system_id
|
||||
"sys_004", # RoundModulation
|
||||
)
|
||||
# "botched", # as default: but with activeFaults, ghost zones & unknown types
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 5.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
151
tests/components/evohome/fixtures/h157546/schedule_10090506.json
Normal file
151
tests/components/evohome/fixtures/h157546/schedule_10090506.json
Normal file
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "21:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "21:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "21:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "22:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "01:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "09:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "11:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "22:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "01:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "09:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "11:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "21:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
123
tests/components/evohome/fixtures/h157546/schedule_10090508.json
Normal file
123
tests/components/evohome/fixtures/h157546/schedule_10090508.json
Normal file
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "09:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 18.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "09:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
115
tests/components/evohome/fixtures/h157546/schedule_10090509.json
Normal file
115
tests/components/evohome/fixtures/h157546/schedule_10090509.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "17:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "17:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "17:30:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "11:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "00:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "11:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 19.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"locationId": "7647411",
|
||||
"gateways": [
|
||||
{
|
||||
"gatewayId": "7539089",
|
||||
"temperatureControlSystems": [
|
||||
{
|
||||
"systemId": "10090510",
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "10090505",
|
||||
"temperatureStatus": {
|
||||
"temperature": 15.5,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 5.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Ba******"
|
||||
},
|
||||
{
|
||||
"zoneId": "10090506",
|
||||
"temperatureStatus": {
|
||||
"temperature": 18.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 18.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Sl********"
|
||||
},
|
||||
{
|
||||
"zoneId": "10090507",
|
||||
"temperatureStatus": {
|
||||
"temperature": 16.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 15.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Ka*********"
|
||||
},
|
||||
{
|
||||
"zoneId": "10090508",
|
||||
"temperatureStatus": {
|
||||
"temperature": 17.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 15.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Ka*****"
|
||||
},
|
||||
{
|
||||
"zoneId": "10090509",
|
||||
"temperatureStatus": {
|
||||
"temperature": 19.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 19.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Wo*******"
|
||||
}
|
||||
],
|
||||
"activeFaults": [],
|
||||
"systemModeStatus": {
|
||||
"mode": "Auto",
|
||||
"isPermanent": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"activeFaults": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user