forked from home-assistant/core
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 918499a85c | |||
| 46ef578986 | |||
| 86162eb660 | |||
| 7f7a33b027 | |||
| 867df99353 | |||
| 283e9d073b | |||
| 38f26376a1 | |||
| 0322dd0e0f | |||
| 3798802557 | |||
| f7833bdbd4 | |||
| e3a156c9b7 | |||
| 6247ec73a3 | |||
| 3feda06e60 | |||
| 56e895fdd4 | |||
| 541506cbdb | |||
| 1f4cda6282 | |||
| 6f77d0b0d5 | |||
| 7976e1b104 | |||
| 1c260cfb00 | |||
| 8424f179e4 | |||
| 00a14a0824 | |||
| 34bec1c50f | |||
| 1d0c520f64 | |||
| d51eda40b3 | |||
| 2d3259413a | |||
| 7a7bd9c621 | |||
| 8ce0b6b4b3 | |||
| 63679333cc | |||
| 5b12bdca00 | |||
| 99e13278e3 | |||
| 07a03ee10d | |||
| fb9f8e3581 | |||
| ee125cd9a4 | |||
| 35a1429e2b | |||
| 89916b38e9 | |||
| c560439545 | |||
| 7322be2006 | |||
| e95ed12ba1 | |||
| 16f36912db | |||
| 2a5f031ba5 | |||
| 71bb8ae529 | |||
| f6a94d0661 | |||
| c2575735ff | |||
| 7eee5ecd9a | |||
| db0cf9fbf4 | |||
| aded44ee0f | |||
| 901926e8e6 | |||
| 2e336626ac | |||
| 3ea3d77f4d | |||
| fe8e7b73bf | |||
| a34065ee2f | |||
| e1a908c8ac | |||
| 628d99886a | |||
| 6b10710484 | |||
| 8a4f28fa94 | |||
| e7331633c7 | |||
| 934be08a59 | |||
| 485522fd76 | |||
| eba0daa2e9 | |||
| 851779e7ad | |||
| ea9a0f4bf5 | |||
| 1143468eb5 | |||
| 52b0b1e2ab | |||
| 23ba652b83 | |||
| 43b737c4a2 | |||
| 3fbd23b98d | |||
| 0cbeeebd0b | |||
| 99a0679ee9 | |||
| e82713b68c | |||
| 9293afd95a | |||
| 9ea3e786f6 | |||
| a8169d2056 | |||
| 1cd94affd1 | |||
| 724825d34c |
Generated
+2
-2
@@ -1678,8 +1678,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam
|
||||
/tests/components/voip/ @balloob @synesthesiam
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
/tests/components/volumio/ @OnFreund
|
||||
/homeassistant/components/volvooncall/ @molobrakos
|
||||
|
||||
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
||||
2: "moderate",
|
||||
3: "high",
|
||||
4: "very_high",
|
||||
5: "extreme",
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"extreme": "Extreme",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "Moderate",
|
||||
@@ -89,6 +90,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -123,6 +125,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -167,6 +170,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -181,6 +185,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -195,6 +200,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
|
||||
@@ -202,7 +202,7 @@ class BackupConfig:
|
||||
if agent_id not in self.data.agents:
|
||||
old_agent_retention = None
|
||||
self.data.agents[agent_id] = AgentConfig(
|
||||
protected=agent_config.get("protected", False),
|
||||
protected=agent_config.get("protected", True),
|
||||
retention=new_agent_retention,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"bleak==0.22.3",
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-auto-recovery==1.5.1",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.45.0"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
(
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language]
|
||||
),
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
@@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
class CloudProvider(Provider):
|
||||
"""Home Assistant Cloud speech API provider."""
|
||||
|
||||
has_entity = True
|
||||
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize cloud provider."""
|
||||
self.cloud = cloud
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.2.0"]
|
||||
"requirements": ["aiodns==3.3.0"]
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@@ -31,6 +32,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
@@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
):
|
||||
return
|
||||
if entry.source == SOURCE_IGNORE:
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
updates: dict[str, Any] = {}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"requirements": [
|
||||
"aioesphomeapi==30.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.14.0"
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ESPHomeDashboardCoordinator
|
||||
from .dashboard import async_get_dashboard
|
||||
from .domain_data import DomainData
|
||||
from .entity import (
|
||||
EsphomeEntity,
|
||||
convert_api_error_ha_error,
|
||||
@@ -62,7 +61,7 @@ async def async_setup_entry(
|
||||
|
||||
if (dashboard := async_get_dashboard(hass)) is None:
|
||||
return
|
||||
entry_data = DomainData.get(hass).get_entry_data(entry)
|
||||
entry_data = entry.runtime_data
|
||||
assert entry_data.device_info is not None
|
||||
device_name = entry_data.device_info.name
|
||||
unsubs: list[CALLBACK_TYPE] = []
|
||||
|
||||
@@ -45,7 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry)
|
||||
# if this is the last entry, remove the storage
|
||||
if len(entries) == 1:
|
||||
hass.data.pop(MY_KEY)
|
||||
return await hass.config_entries.async_unload_platforms(entry, Platform.EVENT)
|
||||
return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT])
|
||||
|
||||
|
||||
async def _async_update_listener(
|
||||
|
||||
@@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
self.check_active_or_lock_mode()
|
||||
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||
await self.async_set_hkr_state("off")
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
@@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new operation mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_hvac_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
if self.hvac_mode is hvac_mode:
|
||||
LOGGER.debug(
|
||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||
@@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_preset_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||
|
||||
@property
|
||||
@@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
|
||||
|
||||
return attrs
|
||||
|
||||
def check_active_or_lock_mode(self) -> None:
|
||||
"""Check if in summer/vacation mode or lock enabled."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_active_mode",
|
||||
)
|
||||
|
||||
if self.data.lock:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_lock_enabled",
|
||||
)
|
||||
|
||||
@@ -88,11 +88,11 @@
|
||||
"manual_switching_disabled": {
|
||||
"message": "Can't toggle switch while manual switching is disabled for the device."
|
||||
},
|
||||
"change_preset_while_active_mode": {
|
||||
"message": "Can't change preset while holiday or summer mode is active on the device."
|
||||
"change_settings_while_lock_enabled": {
|
||||
"message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device"
|
||||
},
|
||||
"change_hvac_while_active_mode": {
|
||||
"message": "Can't change HVAC mode while holiday or summer mode is active on the device."
|
||||
"change_settings_while_active_mode": {
|
||||
"message": "Can't change settings while holiday or summer mode is active on the device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250430.2"]
|
||||
"requirements": ["home-assistant-frontend==20250502.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
delete = _handle
|
||||
patch = _handle
|
||||
options = _handle
|
||||
head = _handle
|
||||
|
||||
async def _handle_websocket(
|
||||
self, request: web.Request, token: str, path: str
|
||||
|
||||
@@ -1551,31 +1551,39 @@
|
||||
}
|
||||
},
|
||||
"coffee_counter": {
|
||||
"name": "Coffees"
|
||||
"name": "Coffees",
|
||||
"unit_of_measurement": "coffees"
|
||||
},
|
||||
"powder_coffee_counter": {
|
||||
"name": "Powder coffees"
|
||||
"name": "Powder coffees",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]"
|
||||
},
|
||||
"hot_water_counter": {
|
||||
"name": "Hot water"
|
||||
},
|
||||
"hot_water_cups_counter": {
|
||||
"name": "Hot water cups"
|
||||
"name": "Hot water cups",
|
||||
"unit_of_measurement": "cups"
|
||||
},
|
||||
"hot_milk_counter": {
|
||||
"name": "Hot milk cups"
|
||||
"name": "Hot milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"frothy_milk_counter": {
|
||||
"name": "Frothy milk cups"
|
||||
"name": "Frothy milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"milk_counter": {
|
||||
"name": "Milk cups"
|
||||
"name": "Milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"coffee_and_milk_counter": {
|
||||
"name": "Coffee and milk cups"
|
||||
"name": "Coffee and milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"ristretto_espresso_counter": {
|
||||
"name": "Ristretto espresso cups"
|
||||
"name": "Ristretto espresso cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"battery_level": {
|
||||
"name": "Battery level"
|
||||
|
||||
@@ -9,10 +9,10 @@ from typing import Any
|
||||
|
||||
from homematicip.async_home import AsyncHome
|
||||
from homematicip.auth import Auth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.base.enums import EventType
|
||||
from homematicip.connection.connection_context import ConnectionContextBuilder
|
||||
from homematicip.connection.rest_connection import RestConnection
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
import homeassistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.1"]
|
||||
"requirements": ["homematicip==2.0.1.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.4.4"]
|
||||
"requirements": ["aioautomower==2025.5.1"]
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"local_name": "ITH-21-B",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "IBS-P02B",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "Ink@IAM-T1",
|
||||
"connectable": true
|
||||
@@ -49,5 +53,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.15.0"]
|
||||
"requirements": ["inkbird-ble==0.16.1"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http, sensor
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as BUTTON_DOMAIN,
|
||||
SERVICE_PRESS as SERVICE_PRESS_BUTTON,
|
||||
ButtonDeviceClass,
|
||||
)
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@@ -20,6 +25,7 @@ from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
)
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN
|
||||
from homeassistant.components.lock import (
|
||||
DOMAIN as LOCK_DOMAIN,
|
||||
SERVICE_LOCK,
|
||||
@@ -80,6 +86,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
ONOFF_DEVICE_CLASSES = {
|
||||
ButtonDeviceClass,
|
||||
CoverDeviceClass,
|
||||
ValveDeviceClass,
|
||||
SwitchDeviceClass,
|
||||
@@ -103,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
intent.INTENT_TURN_ON,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
description="Turns on/opens a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
||||
description="Turns on/opens/presses a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
||||
device_classes=ONOFF_DEVICE_CLASSES,
|
||||
),
|
||||
)
|
||||
@@ -168,6 +175,25 @@ class OnOffIntentHandler(intent.ServiceIntentHandler):
|
||||
"""Call service on entity with handling for special cases."""
|
||||
hass = intent_obj.hass
|
||||
|
||||
if state.domain in (BUTTON_DOMAIN, INPUT_BUTTON_DOMAIN):
|
||||
if service != SERVICE_TURN_ON:
|
||||
raise intent.IntentHandleError(
|
||||
f"Entity {state.entity_id} cannot be turned off"
|
||||
)
|
||||
|
||||
await self._run_then_background(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
state.domain,
|
||||
SERVICE_PRESS_BUTTON,
|
||||
{ATTR_ENTITY_ID: state.entity_id},
|
||||
context=intent_obj.context,
|
||||
blocking=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if state.domain == COVER_DOMAIN:
|
||||
# on = open
|
||||
# off = close
|
||||
|
||||
@@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
|
||||
for dtype, _, node_id in folder.children:
|
||||
if dtype != TAG_FOLDER:
|
||||
continue
|
||||
entity_folder = folder[node_id]
|
||||
|
||||
entity_folder: Programs = folder[node_id]
|
||||
actions = None
|
||||
status = entity_folder.get_by_name(KEY_STATUS)
|
||||
if not status or status.protocol != PROTO_PROGRAM:
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.4.0"],
|
||||
"requirements": ["pyisy==3.4.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
||||
@@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None):
|
||||
_LOGGER.debug("Unknown media type received: %s", media_content_type)
|
||||
raise UnknownMediaType from err
|
||||
|
||||
thumbnail = item.get("thumbnail")
|
||||
if "art" in item:
|
||||
thumbnail = item["art"].get("poster", item.get("thumbnail"))
|
||||
else:
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail is not None and get_thumbnail_url is not None:
|
||||
thumbnail = await get_thumbnail_url(
|
||||
media_content_type, media_content_id, thumbnail_url=thumbnail
|
||||
@@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
title = None
|
||||
media = None
|
||||
|
||||
properties = ["thumbnail"]
|
||||
properties = ["thumbnail", "art"]
|
||||
if search_type == MediaType.ALBUM:
|
||||
if search_id:
|
||||
album = await media_library.get_album_details(
|
||||
album_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
album["albumdetails"].get("thumbnail")
|
||||
album["albumdetails"]["art"].get(
|
||||
"poster", album["albumdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = album["albumdetails"]["label"]
|
||||
media = await media_library.get_songs(
|
||||
@@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
"album",
|
||||
"thumbnail",
|
||||
"track",
|
||||
"art",
|
||||
],
|
||||
)
|
||||
media = media.get("songs")
|
||||
@@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
artist_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
artist["artistdetails"].get("thumbnail")
|
||||
artist["artistdetails"]["art"].get(
|
||||
"poster", artist["artistdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = artist["artistdetails"]["label"]
|
||||
else:
|
||||
@@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
movie_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
movie["moviedetails"].get("thumbnail")
|
||||
movie["moviedetails"]["art"].get(
|
||||
"poster", movie["moviedetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = movie["moviedetails"]["label"]
|
||||
else:
|
||||
media = await media_library.get_movies(properties)
|
||||
media = media.get("movies")
|
||||
@@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
if search_id:
|
||||
media = await media_library.get_seasons(
|
||||
tv_show_id=int(search_id),
|
||||
properties=["thumbnail", "season", "tvshowid"],
|
||||
properties=["thumbnail", "season", "tvshowid", "art"],
|
||||
)
|
||||
media = media.get("seasons")
|
||||
tvshow = await media_library.get_tv_show_details(
|
||||
tv_show_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
tvshow["tvshowdetails"].get("thumbnail")
|
||||
tvshow["tvshowdetails"]["art"].get(
|
||||
"poster", tvshow["tvshowdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = tvshow["tvshowdetails"]["label"]
|
||||
else:
|
||||
@@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
media = await media_library.get_episodes(
|
||||
tv_show_id=int(tv_show_id),
|
||||
season_id=int(season_id),
|
||||
properties=["thumbnail", "tvshowid", "seasonid"],
|
||||
properties=["thumbnail", "tvshowid", "seasonid", "art"],
|
||||
)
|
||||
media = media.get("episodes")
|
||||
if media:
|
||||
@@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
season_id=int(media[0]["seasonid"]), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
season["seasondetails"].get("thumbnail")
|
||||
season["seasondetails"]["art"].get(
|
||||
"poster", season["seasondetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = season["seasondetails"]["label"]
|
||||
|
||||
@@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
|
||||
)
|
||||
media = media.get("channels")
|
||||
|
||||
title = "Channels"
|
||||
|
||||
return thumbnail, title, media
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_USE_BLUETOOTH, DOMAIN
|
||||
from .coordinator import (
|
||||
@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
assert entry.unique_id
|
||||
serial = entry.unique_id
|
||||
|
||||
client = async_create_clientsession(hass)
|
||||
client = async_get_clientsession(hass)
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from ex
|
||||
except RequestNotSuccessful as ex:
|
||||
except (RequestNotSuccessful, TimeoutError) as ex:
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN, translation_key="api_error"
|
||||
|
||||
@@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
).status
|
||||
is MachineState.BREWING
|
||||
),
|
||||
available_fn=lambda device: device.websocket.connected,
|
||||
available_fn=lambda coordinator: not coordinator.websocket_terminated,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
LaMarzoccoBinarySensorEntityDescription(
|
||||
|
||||
@@ -33,7 +33,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
**user_input,
|
||||
}
|
||||
|
||||
self._client = async_create_clientsession(self.hass)
|
||||
self._client = async_get_clientsession(self.hass)
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=data[CONF_USERNAME],
|
||||
password=data[CONF_PASSWORD],
|
||||
|
||||
@@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
_default_update_interval = SCAN_INTERVAL
|
||||
config_entry: LaMarzoccoConfigEntry
|
||||
websocket_terminated = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,25 +93,37 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
await self.device.get_dashboard()
|
||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||
|
||||
_LOGGER.debug("Init WebSocket in background task")
|
||||
|
||||
self.config_entry.async_create_background_task(
|
||||
hass=self.hass,
|
||||
target=self.device.connect_dashboard_websocket(
|
||||
update_callback=lambda _: self.async_set_updated_data(None)
|
||||
),
|
||||
target=self.connect_websocket(),
|
||||
name="lm_websocket_task",
|
||||
)
|
||||
|
||||
async def websocket_close(_: Any | None = None) -> None:
|
||||
if self.device.websocket.connected:
|
||||
await self.device.websocket.disconnect()
|
||||
await self.device.websocket.disconnect()
|
||||
|
||||
self.config_entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close)
|
||||
)
|
||||
self.config_entry.async_on_unload(websocket_close)
|
||||
|
||||
async def connect_websocket(self) -> None:
|
||||
"""Connect to the websocket."""
|
||||
|
||||
_LOGGER.debug("Init WebSocket in background task")
|
||||
|
||||
self.websocket_terminated = False
|
||||
self.async_update_listeners()
|
||||
|
||||
await self.device.connect_dashboard_websocket(
|
||||
update_callback=lambda _: self.async_set_updated_data(None),
|
||||
connect_callback=self.async_update_listeners,
|
||||
disconnect_callback=self.async_update_listeners,
|
||||
)
|
||||
|
||||
self.websocket_terminated = True
|
||||
self.async_update_listeners()
|
||||
|
||||
|
||||
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
"""Coordinator for La Marzocco settings."""
|
||||
|
||||
@@ -5,8 +5,10 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_MAC, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_USE_BLUETOOTH
|
||||
from .coordinator import LaMarzoccoConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
@@ -21,4 +23,12 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data.config_coordinator
|
||||
device = coordinator.device
|
||||
return async_redact_data(device.to_dict(), TO_REDACT)
|
||||
data = {
|
||||
"device": device.to_dict(),
|
||||
"bluetooth_available": {
|
||||
"options_enabled": entry.options.get(CONF_USE_BLUETOOTH, True),
|
||||
CONF_MAC: CONF_MAC in entry.data,
|
||||
CONF_TOKEN: CONF_TOKEN in entry.data,
|
||||
},
|
||||
}
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import FirmwareType
|
||||
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_MAC
|
||||
@@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator
|
||||
class LaMarzoccoEntityDescription(EntityDescription):
|
||||
"""Description for all LM entities."""
|
||||
|
||||
available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True
|
||||
available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||
supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||
|
||||
|
||||
@@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if super().available:
|
||||
return self.entity_description.available_fn(self.coordinator.device)
|
||||
return self.entity_description.available_fn(self.coordinator)
|
||||
return False
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.0b6"]
|
||||
"requirements": ["pylamarzocco==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
lambda coordinator: cast(
|
||||
PreBrewing,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||
).mode
|
||||
is PreExtractionMode.PREINFUSION
|
||||
),
|
||||
@@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_in
|
||||
),
|
||||
available_fn=lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
available_fn=lambda coordinator: cast(
|
||||
PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING,
|
||||
supported_fn=(
|
||||
@@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
lambda coordinator: cast(
|
||||
PreBrewing,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING
|
||||
),
|
||||
|
||||
@@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
|
||||
|
||||
else:
|
||||
brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
|
||||
brightness_pct = round(brightness / 255 * 100)
|
||||
brightness = round(
|
||||
(brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255
|
||||
)
|
||||
|
||||
params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
|
||||
|
||||
|
||||
@@ -89,20 +89,27 @@ class LocalCalendarEntity(CalendarEntity):
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
def events_in_range() -> list[CalendarEvent]:
|
||||
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
return await self.hass.async_add_executor_job(events_in_range)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update entity state with the next upcoming event."""
|
||||
now = dt_util.now()
|
||||
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
self._event = _get_calendar_event(event)
|
||||
else:
|
||||
self._event = None
|
||||
|
||||
def next_event() -> CalendarEvent | None:
|
||||
now = dt_util.now()
|
||||
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
|
||||
self._event = await self.hass.async_add_executor_job(next_event)
|
||||
|
||||
async def _async_store(self) -> None:
|
||||
"""Persist the calendar to disk."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
"description": "The term to search for."
|
||||
},
|
||||
"media_filter_classes": {
|
||||
"name": "Media filter classes",
|
||||
"name": "Media class filter",
|
||||
"description": "List of media classes to filter the search results by."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +131,7 @@ class MieleButton(MieleEntity, ButtonEntity):
|
||||
|
||||
return (
|
||||
super().available
|
||||
and self.entity_description.press_data
|
||||
in self.coordinator.data.actions[self._device_id].process_actions
|
||||
and self.entity_description.press_data in self.action.process_actions
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
t_key = ZONE1_DEVICES.get(
|
||||
cast(MieleAppliance, self.device.device_type), "zone_1"
|
||||
)
|
||||
if self.device.device_type in (
|
||||
MieleAppliance.FRIDGE,
|
||||
MieleAppliance.FREEZER,
|
||||
):
|
||||
self._attr_name = None
|
||||
|
||||
if description.zone == 2:
|
||||
if self.device.device_type in (
|
||||
@@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature."""
|
||||
if self.entity_description.target_fn(self.device) is None:
|
||||
return None
|
||||
|
||||
return cast(float | None, self.entity_description.target_fn(self.device))
|
||||
|
||||
@property
|
||||
@@ -201,9 +205,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
"""Return the maximum target temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.coordinator.data.actions[self._device_id]
|
||||
.target_temperature[self.entity_description.zone - 1]
|
||||
.max,
|
||||
self.action.target_temperature[self.entity_description.zone - 1].max,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -211,9 +213,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
"""Return the minimum target temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.coordinator.data.actions[self._device_id]
|
||||
.target_temperature[self.entity_description.zone - 1]
|
||||
.min,
|
||||
self.action.target_temperature[self.entity_description.zone - 1].min,
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -47,7 +47,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
|
||||
return self.coordinator.data.devices[self._device_id]
|
||||
|
||||
@property
|
||||
def actions(self) -> MieleAction:
|
||||
def action(self) -> MieleAction:
|
||||
"""Return the actions object."""
|
||||
return self.coordinator.data.actions[self._device_id]
|
||||
|
||||
|
||||
@@ -99,8 +99,10 @@ class MieleFan(MieleEntity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return current on/off state."""
|
||||
assert self.device.state_ventilation_step is not None
|
||||
return self.device.state_ventilation_step > 0
|
||||
return (
|
||||
self.device.state_ventilation_step is not None
|
||||
and self.device.state_ventilation_step > 0
|
||||
)
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pymiele==0.4.1"],
|
||||
"requirements": ["pymiele==0.4.3"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleAppliance.STEAM_OVEN,
|
||||
MieleAppliance.MICROWAVE,
|
||||
MieleAppliance.COFFEE_SYSTEM,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER,
|
||||
MieleAppliance.WASHER_DRYER,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MICRO,
|
||||
|
||||
@@ -115,9 +115,6 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"door": {
|
||||
"name": "Door"
|
||||
},
|
||||
"failure": {
|
||||
"name": "Failure"
|
||||
},
|
||||
|
||||
@@ -169,15 +169,14 @@ class MielePowerSwitch(MieleSwitch):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the switch."""
|
||||
return self.coordinator.data.actions[self._device_id].power_off_enabled
|
||||
return self.action.power_off_enabled
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the entity."""
|
||||
|
||||
return (
|
||||
self.coordinator.data.actions[self._device_id].power_off_enabled
|
||||
or self.coordinator.data.actions[self._device_id].power_on_enabled
|
||||
self.action.power_off_enabled or self.action.power_on_enabled
|
||||
) and super().available
|
||||
|
||||
async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None:
|
||||
@@ -192,12 +191,8 @@ class MielePowerSwitch(MieleSwitch):
|
||||
"entity": self.entity_id,
|
||||
},
|
||||
) from err
|
||||
self.coordinator.data.actions[self._device_id].power_on_enabled = cast(
|
||||
bool, mode
|
||||
)
|
||||
self.coordinator.data.actions[self._device_id].power_off_enabled = not cast(
|
||||
bool, mode
|
||||
)
|
||||
self.action.power_on_enabled = cast(bool, mode)
|
||||
self.action.power_off_enabled = not cast(bool, mode)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ class PlatformField:
|
||||
required: bool
|
||||
validator: Callable[..., Any]
|
||||
error: str | None = None
|
||||
default: str | int | bool | vol.Undefined = vol.UNDEFINED
|
||||
default: str | int | bool | None | vol.Undefined = vol.UNDEFINED
|
||||
is_schema_default: bool = False
|
||||
exclude_from_reconfig: bool = False
|
||||
conditions: tuple[dict[str, Any], ...] | None = None
|
||||
@@ -515,6 +515,7 @@ COMMON_ENTITY_FIELDS = {
|
||||
required=False,
|
||||
validator=str,
|
||||
exclude_from_reconfig=True,
|
||||
default=None,
|
||||
),
|
||||
CONF_ENTITY_PICTURE: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
|
||||
@@ -1150,7 +1151,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
|
||||
}
|
||||
|
||||
MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str),
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str),
|
||||
ATTR_SW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=str
|
||||
),
|
||||
@@ -1324,7 +1325,10 @@ def data_schema_from_fields(
|
||||
vol.Required(field_name, default=field_details.default)
|
||||
if field_details.required
|
||||
else vol.Optional(
|
||||
field_name, default=field_details.default
|
||||
field_name,
|
||||
default=field_details.default
|
||||
if field_details.default is not None
|
||||
else vol.UNDEFINED,
|
||||
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
|
||||
if field_details.custom_filtering
|
||||
else field_details.selector
|
||||
@@ -1375,12 +1379,14 @@ def data_schema_from_fields(
|
||||
@callback
|
||||
def subentry_schema_default_data_from_fields(
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
component_data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Generate custom data schema from platform fields or device data."""
|
||||
return {
|
||||
key: field.default
|
||||
for key, field in data_schema_fields.items()
|
||||
if field.is_schema_default
|
||||
or (field.default is not vol.UNDEFINED and key not in component_data)
|
||||
}
|
||||
|
||||
|
||||
@@ -2206,7 +2212,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
for component_data in self._subentry_data["components"].values():
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
subentry_default_data = subentry_schema_default_data_from_fields(
|
||||
PLATFORM_ENTITY_FIELDS[platform]
|
||||
PLATFORM_ENTITY_FIELDS[platform] | COMMON_ENTITY_FIELDS, component_data
|
||||
)
|
||||
component_data.update(subentry_default_data)
|
||||
|
||||
|
||||
@@ -256,8 +256,8 @@
|
||||
"green_template": "Green template",
|
||||
"last_reset_value_template": "Last reset value template",
|
||||
"optimistic": "Optimistic",
|
||||
"payload_off": "Payload off",
|
||||
"payload_on": "Payload on",
|
||||
"payload_off": "Payload \"off\"",
|
||||
"payload_on": "Payload \"on\"",
|
||||
"qos": "QoS",
|
||||
"red_template": "Red template",
|
||||
"retain": "Retain",
|
||||
@@ -278,7 +278,7 @@
|
||||
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
||||
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
|
||||
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
|
||||
"on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.",
|
||||
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
|
||||
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
||||
"payload_off": "The payload that represents the off state.",
|
||||
"payload_on": "The payload that represents the on state.",
|
||||
@@ -287,7 +287,7 @@
|
||||
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
|
||||
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
|
||||
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
|
||||
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
||||
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
||||
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
|
||||
},
|
||||
"sections": {
|
||||
@@ -325,7 +325,7 @@
|
||||
"data_description": {
|
||||
"brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.",
|
||||
"brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.",
|
||||
"brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)",
|
||||
"brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)",
|
||||
"brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.",
|
||||
"brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)",
|
||||
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
|
||||
@@ -385,7 +385,7 @@
|
||||
"hs_value_template": "HS value template"
|
||||
},
|
||||
"data_description": {
|
||||
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.",
|
||||
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.",
|
||||
"hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)",
|
||||
"hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)",
|
||||
"hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value."
|
||||
@@ -574,15 +574,15 @@
|
||||
"discovery": "Option to enable MQTT automatic discovery.",
|
||||
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
|
||||
"birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.",
|
||||
"birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.",
|
||||
"birth_payload": "The `birth` message that is published when MQTT is ready and connected.",
|
||||
"birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected",
|
||||
"birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.",
|
||||
"will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.",
|
||||
"will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.",
|
||||
"will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.",
|
||||
"will_qos": "The quality of service of the `will` message that is published by your MQTT broker.",
|
||||
"will_retain": "When set, your MQTT broker will retain the `will` message."
|
||||
"birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.",
|
||||
"birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.",
|
||||
"birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected",
|
||||
"birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.",
|
||||
"will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.",
|
||||
"will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.",
|
||||
"will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.",
|
||||
"will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.",
|
||||
"will_retain": "When set, your MQTT broker will retain the \"will\" message."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==2.0.5"]
|
||||
"requirements": ["py-nextbusnext==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -113,14 +113,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
_LOGGER.error("Error getting accounts: %s", err)
|
||||
raise
|
||||
for account in accounts:
|
||||
id_prefix = "_".join(
|
||||
id_prefix = (
|
||||
(
|
||||
self.api.utility.subdomain(),
|
||||
account.meter_type.name.lower(),
|
||||
# Some utilities like AEP have "-" in their account id.
|
||||
# Replace it with "_" to avoid "Invalid statistic_id"
|
||||
account.utility_account_id.replace("-", "_").lower(),
|
||||
f"{self.api.utility.subdomain()}_{account.meter_type.name}_"
|
||||
f"{account.utility_account_id}"
|
||||
)
|
||||
# Some utilities like AEP have "-" in their account id.
|
||||
# Other utilities like ngny-gas have "-" in their subdomain.
|
||||
# Replace it with "_" to avoid "Invalid statistic_id"
|
||||
.replace("-", "_")
|
||||
.lower()
|
||||
)
|
||||
cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost"
|
||||
compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation"
|
||||
@@ -190,7 +192,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
return_sum = 0.0
|
||||
last_stats_time = None
|
||||
else:
|
||||
await self._async_maybe_migrate_statistics(
|
||||
migrated = await self._async_maybe_migrate_statistics(
|
||||
account.utility_account_id,
|
||||
{
|
||||
cost_statistic_id: compensation_statistic_id,
|
||||
@@ -203,6 +205,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
return_statistic_id: return_metadata,
|
||||
},
|
||||
)
|
||||
if migrated:
|
||||
# Skip update to avoid working on old data since the migration is done
|
||||
# asynchronously. Update the statistics in the next refresh in 12h.
|
||||
_LOGGER.debug(
|
||||
"Statistics migration completed. Skipping update for now"
|
||||
)
|
||||
continue
|
||||
cost_reads = await self._async_get_cost_reads(
|
||||
account,
|
||||
self.api.utility.timezone(),
|
||||
@@ -326,7 +335,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
utility_account_id: str,
|
||||
migration_map: dict[str, str],
|
||||
metadata_map: dict[str, StatisticMetaData],
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Perform one-time statistics migration based on the provided map.
|
||||
|
||||
Splits negative values from source IDs into target IDs.
|
||||
@@ -339,7 +348,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
|
||||
"""
|
||||
if not migration_map:
|
||||
return
|
||||
return False
|
||||
|
||||
need_migration_source_ids = set()
|
||||
for source_id, target_id in migration_map.items():
|
||||
@@ -349,12 +358,12 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
1,
|
||||
target_id,
|
||||
True,
|
||||
{},
|
||||
set(),
|
||||
)
|
||||
if not last_target_stat:
|
||||
need_migration_source_ids.add(source_id)
|
||||
if not need_migration_source_ids:
|
||||
return
|
||||
return False
|
||||
|
||||
_LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids)
|
||||
|
||||
@@ -416,7 +425,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
|
||||
if not need_migration_source_ids:
|
||||
_LOGGER.debug("No migration needed")
|
||||
return
|
||||
return False
|
||||
|
||||
for stat_id, stats in processed_stats.items():
|
||||
_LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id)
|
||||
@@ -434,7 +443,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
"energy_settings": "/config/energy",
|
||||
"target_ids": "\n".join(
|
||||
{
|
||||
v
|
||||
str(metadata_map[v]["name"])
|
||||
for k, v in migration_map.items()
|
||||
if k in need_migration_source_ids
|
||||
}
|
||||
@@ -442,6 +451,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def _async_get_cost_reads(
|
||||
self, account: Account, time_zone_str: str, start_time: float | None = None
|
||||
) -> list[CostRead]:
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"issues": {
|
||||
"return_to_grid_migration": {
|
||||
"title": "Return to grid statistics for account: {utility_account_id}",
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}"
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,15 @@ class PicoProvider(Provider):
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
||||
fname = tmpf.name
|
||||
|
||||
cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message]
|
||||
subprocess.call(cmd)
|
||||
cmd = ["pico2wave", "--wave", fname, "-l", language]
|
||||
result = subprocess.run(cmd, text=True, input=message, check=False)
|
||||
data = None
|
||||
try:
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error(
|
||||
"Error running pico2wave, return code: %s", result.returncode
|
||||
)
|
||||
return (None, None)
|
||||
with open(fname, "rb") as voice:
|
||||
data = voice.read()
|
||||
except OSError:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pushover",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pushover_complete"],
|
||||
"requirements": ["pushover_complete==1.1.1"]
|
||||
"requirements": ["pushover_complete==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||
@@ -134,6 +135,7 @@
|
||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
"""Set up Rehlko from a config entry."""
|
||||
websession = async_get_clientsession(hass)
|
||||
rehlko = AioKem(session=websession)
|
||||
# If requests take more than 20 seconds; timeout and let the setup retry.
|
||||
rehlko.set_timeout(20)
|
||||
|
||||
async def async_refresh_token_update(refresh_token: str) -> None:
|
||||
"""Handle refresh token update."""
|
||||
@@ -40,7 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
)
|
||||
|
||||
rehlko.set_refresh_token_callback(async_refresh_token_update)
|
||||
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
||||
|
||||
try:
|
||||
await rehlko.authenticate(
|
||||
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data.get(CONF_REFRESH_TOKEN),
|
||||
)
|
||||
homes = await rehlko.get_homes()
|
||||
except AuthenticationError as ex:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -60,7 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
translation_key="cannot_connect",
|
||||
) from ex
|
||||
coordinators: dict[int, RehlkoUpdateCoordinator] = {}
|
||||
homes = await rehlko.get_homes()
|
||||
|
||||
entry.runtime_data = RehlkoRuntimeData(
|
||||
coordinators=coordinators,
|
||||
@@ -86,6 +87,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinators[device_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# Retrys enabled after successful connection to prevent blocking startup
|
||||
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
||||
# Rehlko service can be slow to respond, increase timeout for polls.
|
||||
rehlko.set_timeout(100)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiokem"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiokem==0.5.6"]
|
||||
"requirements": ["aiokem==0.5.10"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ async def async_setup_entry(
|
||||
"""Set up the remote calendar platform."""
|
||||
coordinator = entry.runtime_data
|
||||
entity = RemoteCalendarEntity(coordinator, entry)
|
||||
async_add_entities([entity])
|
||||
async_add_entities([entity], True)
|
||||
|
||||
|
||||
class RemoteCalendarEntity(
|
||||
@@ -48,25 +48,46 @@ class RemoteCalendarEntity(
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = entry.data[CONF_CALENDAR_NAME]
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._event: CalendarEvent | None = None
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
now = dt_util.now()
|
||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
return self._event
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
def events_in_range() -> list[CalendarEvent]:
|
||||
"""Return all events in the given time range."""
|
||||
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
return await self.hass.async_add_executor_job(events_in_range)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh the timeline.
|
||||
|
||||
This is called when the coordinator updates. Creating the timeline may
|
||||
require walking through the entire calendar and handling recurring
|
||||
events, so it is done as a separate task without blocking the event loop.
|
||||
"""
|
||||
await super().async_update()
|
||||
|
||||
def next_timeline_event() -> CalendarEvent | None:
|
||||
"""Return the next active event."""
|
||||
now = dt_util.now()
|
||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
|
||||
self._event = await self.hass.async_add_executor_job(next_timeline_event)
|
||||
|
||||
|
||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||
|
||||
@@ -5,8 +5,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from httpx import HTTPError, InvalidURL
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.exceptions import CalendarParseError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -14,6 +12,7 @@ from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_CALENDAR_NAME, DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,15 +63,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("An error occurred: %s", err)
|
||||
else:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
IcsCalendarStream.calendar_from_ics, res.text
|
||||
)
|
||||
except CalendarParseError as err:
|
||||
await parse_calendar(self.hass, res.text)
|
||||
except InvalidIcsException:
|
||||
errors["base"] = "invalid_ics_file"
|
||||
_LOGGER.error("Error reading the calendar information: %s", err.message)
|
||||
_LOGGER.debug(
|
||||
"Additional calendar error detail: %s", str(err.detailed_error)
|
||||
)
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CALENDAR_NAME], data=user_input
|
||||
|
||||
@@ -5,8 +5,6 @@ import logging
|
||||
|
||||
from httpx import HTTPError, InvalidURL
|
||||
from ical.calendar import Calendar
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.exceptions import CalendarParseError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
@@ -15,6 +13,7 @@ from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator]
|
||||
|
||||
@@ -56,14 +55,9 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
translation_placeholders={"err": str(err)},
|
||||
) from err
|
||||
try:
|
||||
# calendar_from_ics will dynamically load packages
|
||||
# the first time it is called, so we need to do it
|
||||
# in a separate thread to avoid blocking the event loop
|
||||
self.ics = res.text
|
||||
return await self.hass.async_add_executor_job(
|
||||
IcsCalendarStream.calendar_from_ics, self.ics
|
||||
)
|
||||
except CalendarParseError as err:
|
||||
return await parse_calendar(self.hass, res.text)
|
||||
except InvalidIcsException as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_parse",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Module for parsing ICS content.
|
||||
|
||||
This module exists to fix known issues where calendar providers return calendars
|
||||
that do not follow rfcc5545. This module will attempt to fix the calendar and return
|
||||
a valid calendar object.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ical.calendar import Calendar
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.compat import enable_compat_mode
|
||||
from ical.exceptions import CalendarParseError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InvalidIcsException(Exception):
|
||||
"""Exception to indicate that the ICS content is invalid."""
|
||||
|
||||
|
||||
def _compat_calendar_from_ics(ics: str) -> Calendar:
|
||||
"""Parse the ICS content and return a Calendar object.
|
||||
|
||||
This function is called in a separate thread to avoid blocking the event
|
||||
loop while loading packages or parsing the ICS content for large calendars.
|
||||
|
||||
It uses the `enable_compat_mode` context manager to fix known issues with
|
||||
calendar providers that return invalid calendars.
|
||||
"""
|
||||
with enable_compat_mode(ics) as compat_ics:
|
||||
return IcsCalendarStream.calendar_from_ics(compat_ics)
|
||||
|
||||
|
||||
async def parse_calendar(hass: HomeAssistant, ics: str) -> Calendar:
|
||||
"""Parse the ICS content and return a Calendar object."""
|
||||
try:
|
||||
return await hass.async_add_executor_job(_compat_calendar_from_ics, ics)
|
||||
except CalendarParseError as err:
|
||||
_LOGGER.error("Error parsing calendar information: %s", err.message)
|
||||
_LOGGER.debug("Additional calendar error detail: %s", str(err.detailed_error))
|
||||
raise InvalidIcsException(err.message) from err
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==2.16.1",
|
||||
"vacuum-map-parser-roborock==0.1.2"
|
||||
"python-roborock==2.18.2",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import cast
|
||||
|
||||
from aiobotocore.client import AioBaseClient as S3Client
|
||||
from aiobotocore.session import AioSession
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -32,6 +33,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
"""Set up S3 from a config entry."""
|
||||
|
||||
data = cast(dict, entry.data)
|
||||
# due to https://github.com/home-assistant/core/issues/143995
|
||||
config = Config(
|
||||
request_checksum_calculation="when_required",
|
||||
response_checksum_validation="when_required",
|
||||
)
|
||||
try:
|
||||
session = AioSession()
|
||||
# pylint: disable-next=unnecessary-dunder-call
|
||||
@@ -40,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
endpoint_url=data.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
|
||||
config=config,
|
||||
).__aenter__()
|
||||
await client.head_bucket(Bucket=data[CONF_BUCKET])
|
||||
except ClientError as err:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.5"]
|
||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.7"]
|
||||
}
|
||||
|
||||
@@ -1212,6 +1212,9 @@ def websocket_list_engines(
|
||||
if entity.platform:
|
||||
entity_domains.add(entity.platform.platform_name)
|
||||
for engine_id, provider in hass.data[DATA_TTS_MANAGER].providers.items():
|
||||
if provider.has_entity:
|
||||
continue
|
||||
|
||||
provider_info = {
|
||||
"engine_id": engine_id,
|
||||
"name": provider.name,
|
||||
|
||||
@@ -207,6 +207,7 @@ class Provider:
|
||||
|
||||
hass: HomeAssistant | None = None
|
||||
name: str | None = None
|
||||
has_entity: bool = False
|
||||
|
||||
@property
|
||||
def default_language(self) -> str | None:
|
||||
|
||||
@@ -145,13 +145,20 @@ class TTSMediaSource(MediaSource):
|
||||
return self._engine_item(engine, params)
|
||||
|
||||
# Root. List providers.
|
||||
children = [
|
||||
self._engine_item(engine)
|
||||
for engine in self.hass.data[DATA_TTS_MANAGER].providers
|
||||
] + [
|
||||
self._engine_item(entity.entity_id)
|
||||
for entity in self.hass.data[DATA_COMPONENT].entities
|
||||
]
|
||||
children = sorted(
|
||||
[
|
||||
self._engine_item(engine_id)
|
||||
for engine_id, provider in self.hass.data[
|
||||
DATA_TTS_MANAGER
|
||||
].providers.items()
|
||||
if not provider.has_entity
|
||||
]
|
||||
+ [
|
||||
self._engine_item(entity.entity_id)
|
||||
for entity in self.hass.data[DATA_COMPONENT].entities
|
||||
],
|
||||
key=lambda x: x.title,
|
||||
)
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=None,
|
||||
@@ -173,7 +180,7 @@ class TTSMediaSource(MediaSource):
|
||||
raise BrowseError("Unknown provider")
|
||||
|
||||
if isinstance(engine_instance, TextToSpeechEntity):
|
||||
engine_domain = engine_instance.platform.domain
|
||||
engine_domain = engine_instance.platform.platform_name
|
||||
else:
|
||||
engine_domain = engine
|
||||
|
||||
|
||||
@@ -51,9 +51,9 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_PIPELINE_TIMEOUT_SEC: Final = 30
|
||||
_HANGUP_SEC: Final = 0.5
|
||||
_ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5
|
||||
_ANNOUNCEMENT_AFTER_DELAY: Final = 1.0
|
||||
_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5
|
||||
_ANNOUNCEMENT_RING_TIMEOUT: Final = 30
|
||||
|
||||
|
||||
@@ -132,9 +132,10 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self._processing_tone_done = asyncio.Event()
|
||||
|
||||
self._announcement: AssistSatelliteAnnouncement | None = None
|
||||
self._announcement_future: asyncio.Future[Any] = asyncio.Future()
|
||||
self._announcment_start_time: float = 0.0
|
||||
self._check_announcement_ended_task: asyncio.Task | None = None
|
||||
self._check_announcement_pickup_task: asyncio.Task | None = None
|
||||
self._check_hangup_task: asyncio.Task | None = None
|
||||
self._call_end_future: asyncio.Future[Any] = asyncio.Future()
|
||||
self._last_chunk_time: float | None = None
|
||||
self._rtp_port: int | None = None
|
||||
self._run_pipeline_after_announce: bool = False
|
||||
@@ -233,7 +234,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
translation_key="non_tts_announcement",
|
||||
)
|
||||
|
||||
self._announcement_future = asyncio.Future()
|
||||
self._call_end_future = asyncio.Future()
|
||||
self._run_pipeline_after_announce = run_pipeline_after
|
||||
|
||||
if self._rtp_port is None:
|
||||
@@ -274,53 +275,77 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
rtp_port=self._rtp_port,
|
||||
)
|
||||
|
||||
# Check if caller hung up or didn't pick up
|
||||
self._check_announcement_ended_task = (
|
||||
# Check if caller didn't pick up
|
||||
self._check_announcement_pickup_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_announcement_ended(),
|
||||
"voip_announcement_ended",
|
||||
self._check_announcement_pickup(),
|
||||
"voip_announcement_pickup",
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await self._announcement_future
|
||||
await self._call_end_future
|
||||
except TimeoutError:
|
||||
# Stop ringing
|
||||
_LOGGER.debug("Caller did not pick up in time")
|
||||
sip_protocol.cancel_call(call_info)
|
||||
raise
|
||||
|
||||
async def _check_announcement_ended(self) -> None:
|
||||
async def _check_announcement_pickup(self) -> None:
|
||||
"""Continuously checks if an audio chunk was received within a time limit.
|
||||
|
||||
If not, the caller is presumed to have hung up and the announcement is ended.
|
||||
If not, the caller is presumed to have not picked up the phone and the announcement is ended.
|
||||
"""
|
||||
while self._announcement is not None:
|
||||
while True:
|
||||
current_time = time.monotonic()
|
||||
if (self._last_chunk_time is None) and (
|
||||
(current_time - self._announcment_start_time)
|
||||
> _ANNOUNCEMENT_RING_TIMEOUT
|
||||
):
|
||||
# Ring timeout
|
||||
_LOGGER.debug("Ring timeout")
|
||||
self._announcement = None
|
||||
self._check_announcement_ended_task = None
|
||||
self._announcement_future.set_exception(
|
||||
self._check_announcement_pickup_task = None
|
||||
self._call_end_future.set_exception(
|
||||
TimeoutError("User did not pick up in time")
|
||||
)
|
||||
_LOGGER.debug("Timed out waiting for the user to pick up the phone")
|
||||
break
|
||||
|
||||
if (self._last_chunk_time is not None) and (
|
||||
(current_time - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC
|
||||
):
|
||||
# Caller hung up
|
||||
self._announcement = None
|
||||
self._announcement_future.set_result(None)
|
||||
self._check_announcement_ended_task = None
|
||||
_LOGGER.debug("Announcement ended")
|
||||
if self._last_chunk_time is not None:
|
||||
_LOGGER.debug("Picked up the phone")
|
||||
self._check_announcement_pickup_task = None
|
||||
break
|
||||
|
||||
await asyncio.sleep(_ANNOUNCEMENT_HANGUP_SEC / 2)
|
||||
await asyncio.sleep(_HANGUP_SEC / 2)
|
||||
|
||||
async def _check_hangup(self) -> None:
|
||||
"""Continuously checks if an audio chunk was received within a time limit.
|
||||
|
||||
If not, the caller is presumed to have hung up and the call is ended.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
current_time = time.monotonic()
|
||||
if (self._last_chunk_time is not None) and (
|
||||
(current_time - self._last_chunk_time) > _HANGUP_SEC
|
||||
):
|
||||
# Caller hung up
|
||||
_LOGGER.debug("Hang up")
|
||||
self._announcement = None
|
||||
if self._run_pipeline_task is not None:
|
||||
_LOGGER.debug("Cancelling running pipeline")
|
||||
self._run_pipeline_task.cancel()
|
||||
self._call_end_future.set_result(None)
|
||||
self.disconnect()
|
||||
break
|
||||
|
||||
await asyncio.sleep(_HANGUP_SEC / 2)
|
||||
except asyncio.CancelledError:
|
||||
# Don't swallow cancellation
|
||||
if (current_task := asyncio.current_task()) and current_task.cancelling():
|
||||
raise
|
||||
_LOGGER.debug("Check hangup cancelled")
|
||||
|
||||
async def async_start_conversation(
|
||||
self, start_announcement: AssistSatelliteAnnouncement
|
||||
@@ -332,6 +357,24 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
# VoIP
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def disconnect(self):
|
||||
"""Server disconnected."""
|
||||
super().disconnect()
|
||||
if self._check_hangup_task is not None:
|
||||
self._check_hangup_task.cancel()
|
||||
self._check_hangup_task = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Server is ready."""
|
||||
super().connection_made(transport)
|
||||
self._last_chunk_time = time.monotonic()
|
||||
# Check if caller hung up
|
||||
self._check_hangup_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_hangup(),
|
||||
"voip_hangup",
|
||||
)
|
||||
|
||||
def on_chunk(self, audio_bytes: bytes) -> None:
|
||||
"""Handle raw audio chunk."""
|
||||
self._last_chunk_time = time.monotonic()
|
||||
@@ -368,13 +411,22 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self.voip_device.set_is_active(True)
|
||||
|
||||
async def stt_stream():
|
||||
retry: bool = True
|
||||
while True:
|
||||
async with asyncio.timeout(self._audio_chunk_timeout):
|
||||
chunk = await self._audio_queue.get()
|
||||
if not chunk:
|
||||
break
|
||||
try:
|
||||
async with asyncio.timeout(self._audio_chunk_timeout):
|
||||
chunk = await self._audio_queue.get()
|
||||
if not chunk:
|
||||
_LOGGER.debug("STT stream got None")
|
||||
break
|
||||
|
||||
yield chunk
|
||||
except TimeoutError:
|
||||
_LOGGER.debug("STT Stream timed out")
|
||||
if not retry:
|
||||
_LOGGER.debug("No more retries, ending STT stream")
|
||||
break
|
||||
retry = False
|
||||
|
||||
# Play listening tone at the start of each cycle
|
||||
await self._play_tone(Tones.LISTENING, silence_before=0.2)
|
||||
@@ -385,6 +437,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
)
|
||||
|
||||
if self._pipeline_had_error:
|
||||
_LOGGER.debug("Pipeline error")
|
||||
self._pipeline_had_error = False
|
||||
await self._play_tone(Tones.ERROR)
|
||||
else:
|
||||
@@ -394,7 +447,14 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
# length of the TTS audio.
|
||||
await self._tts_done.wait()
|
||||
except TimeoutError:
|
||||
# This shouldn't happen anymore, we are detecting hang ups with a separate task
|
||||
_LOGGER.exception("Timeout error")
|
||||
self.disconnect() # caller hung up
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Pipeline cancelled")
|
||||
# Don't swallow cancellation
|
||||
if (current_task := asyncio.current_task()) and current_task.cancelling():
|
||||
raise
|
||||
finally:
|
||||
# Stop audio stream
|
||||
await self._audio_queue.put(None)
|
||||
@@ -433,8 +493,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
|
||||
if self._run_pipeline_after_announce:
|
||||
# Clear announcement to allow pipeline to run
|
||||
_LOGGER.debug("Clearing announcement")
|
||||
self._announcement = None
|
||||
self._announcement_future.set_result(None)
|
||||
|
||||
def _clear_audio_queue(self) -> None:
|
||||
"""Ensure audio queue is empty."""
|
||||
@@ -463,6 +523,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
)
|
||||
else:
|
||||
# Empty TTS response
|
||||
_LOGGER.debug("Empty TTS response")
|
||||
self._tts_done.set()
|
||||
elif event.type == PipelineEventType.ERROR:
|
||||
# Play error tone instead of wait for TTS when pipeline is finished.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "voip",
|
||||
"name": "Voice over IP",
|
||||
"codeowners": ["@balloob", "@synesthesiam"],
|
||||
"codeowners": ["@balloob", "@synesthesiam", "@jaminh"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "assist_satellite", "intent", "network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/voip",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["voip_utils"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["voip-utils==0.3.1"]
|
||||
"requirements": ["voip-utils==0.3.2"]
|
||||
}
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/wmspro",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pywmspro==0.2.1"]
|
||||
"requirements": ["pywmspro==0.2.2"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.146.5"]
|
||||
"requirements": ["zeroconf==0.147.0"]
|
||||
}
|
||||
|
||||
@@ -717,7 +717,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
data_schema = vol.Schema(schema)
|
||||
|
||||
return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
|
||||
return self.async_show_form(
|
||||
step_id="configure_addon_user", data_schema=data_schema
|
||||
)
|
||||
|
||||
async def async_step_finish_addon_setup_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -1097,7 +1099,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
|
||||
return self.async_show_form(
|
||||
step_id="configure_addon_reconfigure", data_schema=data_schema
|
||||
)
|
||||
|
||||
async def async_step_choose_serial_port(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
"restore_nvm": "Please wait while the network restore completes."
|
||||
},
|
||||
"step": {
|
||||
"configure_addon": {
|
||||
"configure_addon_user": {
|
||||
"data": {
|
||||
"lr_s2_access_control_key": "Long Range S2 Access Control Key",
|
||||
"lr_s2_authenticated_key": "Long Range S2 Authenticated Key",
|
||||
"s0_legacy_key": "S0 Key (Legacy)",
|
||||
"s2_access_control_key": "S2 Access Control Key",
|
||||
"s2_authenticated_key": "S2 Authenticated Key",
|
||||
@@ -52,14 +54,16 @@
|
||||
"data": {
|
||||
"emulate_hardware": "Emulate Hardware",
|
||||
"log_level": "Log level",
|
||||
"s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]",
|
||||
"s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]",
|
||||
"s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]",
|
||||
"s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]",
|
||||
"lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]",
|
||||
"lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]",
|
||||
"s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]",
|
||||
"s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_access_control_key%]",
|
||||
"s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_authenticated_key%]",
|
||||
"s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_unauthenticated_key%]",
|
||||
"usb_path": "[%key:common::config_flow::data::usb_path%]"
|
||||
},
|
||||
"description": "[%key:component::zwave_js::config::step::configure_addon::description%]",
|
||||
"title": "[%key:component::zwave_js::config::step::configure_addon::title%]"
|
||||
"description": "[%key:component::zwave_js::config::step::configure_addon_user::description%]",
|
||||
"title": "[%key:component::zwave_js::config::step::configure_addon_user::title%]"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"description": "Do you want to set up the Z-Wave integration with the Z-Wave add-on?"
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
__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)
|
||||
|
||||
Generated
+5
@@ -376,6 +376,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||
"domain": "inkbird",
|
||||
"local_name": "ITH-21-B",
|
||||
},
|
||||
{
|
||||
"connectable": False,
|
||||
"domain": "inkbird",
|
||||
"local_name": "IBS-P02B",
|
||||
},
|
||||
{
|
||||
"connectable": True,
|
||||
"domain": "inkbird",
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.util.json import json_loads
|
||||
|
||||
from .frame import warn_use
|
||||
from .json import json_dumps
|
||||
from .singleton import singleton
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiohttp.typedefs import JSONDecoder
|
||||
@@ -39,6 +40,7 @@ DATA_CONNECTOR: HassKey[dict[tuple[bool, int, str], aiohttp.BaseConnector]] = Ha
|
||||
DATA_CLIENTSESSION: HassKey[dict[tuple[bool, int, str], aiohttp.ClientSession]] = (
|
||||
HassKey("aiohttp_clientsession")
|
||||
)
|
||||
DATA_RESOLVER: HassKey[HassAsyncDNSResolver] = HassKey("aiohttp_resolver")
|
||||
|
||||
SERVER_SOFTWARE = (
|
||||
f"{APPLICATION_NAME}/{__version__} "
|
||||
@@ -70,6 +72,21 @@ MAXIMUM_CONNECTIONS = 4096
|
||||
MAXIMUM_CONNECTIONS_PER_HOST = 100
|
||||
|
||||
|
||||
class HassAsyncDNSResolver(AsyncDualMDNSResolver):
|
||||
"""Home Assistant AsyncDNSResolver.
|
||||
|
||||
This is a wrapper around the AsyncDualMDNSResolver to only
|
||||
close the resolver when the Home Assistant instance is closed.
|
||||
"""
|
||||
|
||||
async def real_close(self) -> None:
|
||||
"""Close the resolver."""
|
||||
await super().close()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the resolver."""
|
||||
|
||||
|
||||
class HassClientResponse(aiohttp.ClientResponse):
|
||||
"""aiohttp.ClientResponse with a json method that uses json_loads by default."""
|
||||
|
||||
@@ -363,7 +380,7 @@ def _async_get_connector(
|
||||
ssl=ssl_context,
|
||||
limit=MAXIMUM_CONNECTIONS,
|
||||
limit_per_host=MAXIMUM_CONNECTIONS_PER_HOST,
|
||||
resolver=_async_make_resolver(hass),
|
||||
resolver=_async_get_or_create_resolver(hass),
|
||||
)
|
||||
connectors[connector_key] = connector
|
||||
|
||||
@@ -376,6 +393,19 @@ def _async_get_connector(
|
||||
return connector
|
||||
|
||||
|
||||
@singleton(DATA_RESOLVER)
|
||||
@callback
|
||||
def _async_make_resolver(hass: HomeAssistant) -> AsyncDualMDNSResolver:
|
||||
return AsyncDualMDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass))
|
||||
def _async_get_or_create_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver:
|
||||
"""Return the HassAsyncDNSResolver."""
|
||||
resolver = _async_make_resolver(hass)
|
||||
|
||||
async def _async_close_resolver(event: Event) -> None:
|
||||
await resolver.real_close()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_resolver)
|
||||
return resolver
|
||||
|
||||
|
||||
@callback
|
||||
def _async_make_resolver(hass: HomeAssistant) -> HassAsyncDNSResolver:
|
||||
return HassAsyncDNSResolver(async_zeroconf=zeroconf.async_get_async_zeroconf(hass))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
aiodhcpwatcher==1.1.1
|
||||
aiodiscover==2.6.1
|
||||
aiodns==3.2.0
|
||||
aiodns==3.3.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.2.3
|
||||
@@ -23,7 +23,7 @@ bcrypt==4.2.0
|
||||
bleak-retry-connector==3.9.0
|
||||
bleak==0.22.3
|
||||
bluetooth-adapters==0.21.4
|
||||
bluetooth-auto-recovery==1.4.5
|
||||
bluetooth-auto-recovery==1.5.1
|
||||
bluetooth-data-tools==1.28.1
|
||||
cached-ipaddress==0.10.0
|
||||
certifi>=2021.5.30
|
||||
@@ -34,11 +34,11 @@ dbus-fast==2.43.0
|
||||
fnv-hash-fast==1.5.0
|
||||
go2rtc-client==0.1.2
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==3.45.0
|
||||
habluetooth==3.48.2
|
||||
hass-nabucasa==0.96.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250430.2
|
||||
home-assistant-frontend==20250502.1
|
||||
home-assistant-intents==2025.4.30
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -75,7 +75,7 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
yarl==1.20.0
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.5.0.dev0"
|
||||
version = "2025.5.0b5"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.13.2"
|
||||
dependencies = [
|
||||
"aiodns==3.2.0",
|
||||
"aiodns==3.3.0",
|
||||
# Integrations may depend on hassio integration without listing it to
|
||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||
# Lib can be removed with 2025.11
|
||||
@@ -123,7 +123,7 @@ dependencies = [
|
||||
"voluptuous-openapi==0.0.7",
|
||||
"yarl==1.20.0",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.146.5",
|
||||
"zeroconf==0.147.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
Generated
+2
-2
@@ -3,7 +3,7 @@
|
||||
-c homeassistant/package_constraints.txt
|
||||
|
||||
# Home Assistant Core
|
||||
aiodns==3.2.0
|
||||
aiodns==3.3.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp==3.11.18
|
||||
aiohttp_cors==0.7.0
|
||||
@@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous-openapi==0.0.7
|
||||
yarl==1.20.0
|
||||
webrtc-models==0.3.0
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
Generated
+21
-21
@@ -201,7 +201,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.4.4
|
||||
aioautomower==2025.5.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -223,7 +223,7 @@ aiodhcpwatcher==1.1.1
|
||||
aiodiscover==2.6.1
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==3.2.0
|
||||
aiodns==3.3.0
|
||||
|
||||
# homeassistant.components.duke_energy
|
||||
aiodukeenergy==0.3.0
|
||||
@@ -286,7 +286,7 @@ aiokafka==0.10.0
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.rehlko
|
||||
aiokem==0.5.6
|
||||
aiokem==0.5.10
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-effects==0.3.2
|
||||
@@ -607,7 +607,7 @@ bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.eq3btsmart
|
||||
# homeassistant.components.esphome
|
||||
bleak-esphome==2.14.0
|
||||
bleak-esphome==2.15.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==3.9.0
|
||||
@@ -637,7 +637,7 @@ bluemaestro-ble==0.4.0
|
||||
bluetooth-adapters==0.21.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.4.5
|
||||
bluetooth-auto-recovery==1.5.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.ld2410_ble
|
||||
@@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0
|
||||
habiticalib==0.3.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.45.0
|
||||
habluetooth==3.48.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.96.0
|
||||
@@ -1161,13 +1161,13 @@ hole==0.8.0
|
||||
holidays==0.70
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250430.2
|
||||
home-assistant-frontend==20250502.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.4.30
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.1
|
||||
homematicip==2.0.1.1
|
||||
|
||||
# homeassistant.components.horizon
|
||||
horimote==0.4.1
|
||||
@@ -1200,7 +1200,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.1.0
|
||||
ical==9.2.0
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1239,7 +1239,7 @@ influxdb-client==1.24.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==0.15.0
|
||||
inkbird-ble==0.16.1
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.5.0
|
||||
@@ -1726,7 +1726,7 @@ pulsectl==23.5.2
|
||||
pushbullet.py==0.11.0
|
||||
|
||||
# homeassistant.components.pushover
|
||||
pushover_complete==1.1.1
|
||||
pushover_complete==1.2.0
|
||||
|
||||
# homeassistant.components.pvoutput
|
||||
pvo==2.2.1
|
||||
@@ -1759,7 +1759,7 @@ py-madvr2==1.6.32
|
||||
py-melissa-climate==2.1.4
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py-nextbusnext==2.0.5
|
||||
py-nextbusnext==2.1.2
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.2
|
||||
@@ -2054,7 +2054,7 @@ pyiskra==0.1.15
|
||||
pyiss==1.0.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==3.4.0
|
||||
pyisy==3.4.1
|
||||
|
||||
# homeassistant.components.itach
|
||||
pyitachip2ir==0.0.7
|
||||
@@ -2093,7 +2093,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.0b6
|
||||
pylamarzocco==2.0.0
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -2135,7 +2135,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.4.1
|
||||
pymiele==0.4.3
|
||||
|
||||
# homeassistant.components.xiaomi_tv
|
||||
pymitv==1.4.3
|
||||
@@ -2480,7 +2480,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.16.1
|
||||
python-roborock==2.18.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.39
|
||||
@@ -2577,7 +2577,7 @@ pywilight==0.0.74
|
||||
pywizlight==0.6.2
|
||||
|
||||
# homeassistant.components.wmspro
|
||||
pywmspro==0.2.1
|
||||
pywmspro==0.2.2
|
||||
|
||||
# homeassistant.components.ws66i
|
||||
pyws66i==1.1
|
||||
@@ -2897,7 +2897,7 @@ tesla-powerwall==0.5.2
|
||||
tesla-wall-connector==1.0.2
|
||||
|
||||
# homeassistant.components.teslemetry
|
||||
teslemetry-stream==0.7.5
|
||||
teslemetry-stream==0.7.7
|
||||
|
||||
# homeassistant.components.tessie
|
||||
tessie-api==0.1.1
|
||||
@@ -3007,7 +3007,7 @@ url-normalize==2.2.1
|
||||
uvcclient==0.12.1
|
||||
|
||||
# homeassistant.components.roborock
|
||||
vacuum-map-parser-roborock==0.1.2
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
@@ -3025,7 +3025,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.1
|
||||
voip-utils==0.3.2
|
||||
|
||||
# homeassistant.components.volkszaehler
|
||||
volkszaehler==0.4.0
|
||||
@@ -3156,7 +3156,7 @@ zabbix-utils==2.0.2
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.2
|
||||
|
||||
Generated
+21
-21
@@ -189,7 +189,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.4.4
|
||||
aioautomower==2025.5.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -211,7 +211,7 @@ aiodhcpwatcher==1.1.1
|
||||
aiodiscover==2.6.1
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==3.2.0
|
||||
aiodns==3.3.0
|
||||
|
||||
# homeassistant.components.duke_energy
|
||||
aiodukeenergy==0.3.0
|
||||
@@ -268,7 +268,7 @@ aioimaplib==2.0.1
|
||||
aiokafka==0.10.0
|
||||
|
||||
# homeassistant.components.rehlko
|
||||
aiokem==0.5.6
|
||||
aiokem==0.5.10
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-effects==0.3.2
|
||||
@@ -538,7 +538,7 @@ bimmer-connected[china]==0.17.2
|
||||
|
||||
# homeassistant.components.eq3btsmart
|
||||
# homeassistant.components.esphome
|
||||
bleak-esphome==2.14.0
|
||||
bleak-esphome==2.15.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==3.9.0
|
||||
@@ -562,7 +562,7 @@ bluemaestro-ble==0.4.0
|
||||
bluetooth-adapters==0.21.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.4.5
|
||||
bluetooth-auto-recovery==1.5.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
# homeassistant.components.ld2410_ble
|
||||
@@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0
|
||||
habiticalib==0.3.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.45.0
|
||||
habluetooth==3.48.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.96.0
|
||||
@@ -991,13 +991,13 @@ hole==0.8.0
|
||||
holidays==0.70
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250430.2
|
||||
home-assistant-frontend==20250502.1
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.4.30
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.1
|
||||
homematicip==2.0.1.1
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.20.4
|
||||
@@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.1.0
|
||||
ical==9.2.0
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1054,7 +1054,7 @@ influxdb-client==1.24.0
|
||||
influxdb==5.3.1
|
||||
|
||||
# homeassistant.components.inkbird
|
||||
inkbird-ble==0.15.0
|
||||
inkbird-ble==0.16.1
|
||||
|
||||
# homeassistant.components.insteon
|
||||
insteon-frontend-home-assistant==0.5.0
|
||||
@@ -1428,7 +1428,7 @@ psutil==7.0.0
|
||||
pushbullet.py==0.11.0
|
||||
|
||||
# homeassistant.components.pushover
|
||||
pushover_complete==1.1.1
|
||||
pushover_complete==1.2.0
|
||||
|
||||
# homeassistant.components.pvoutput
|
||||
pvo==2.2.1
|
||||
@@ -1461,7 +1461,7 @@ py-madvr2==1.6.32
|
||||
py-melissa-climate==2.1.4
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py-nextbusnext==2.0.5
|
||||
py-nextbusnext==2.1.2
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.2
|
||||
@@ -1678,7 +1678,7 @@ pyiskra==0.1.15
|
||||
pyiss==1.0.1
|
||||
|
||||
# homeassistant.components.isy994
|
||||
pyisy==3.4.0
|
||||
pyisy==3.4.1
|
||||
|
||||
# homeassistant.components.ituran
|
||||
pyituran==0.1.4
|
||||
@@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.0b6
|
||||
pylamarzocco==2.0.0
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -1747,7 +1747,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.4.1
|
||||
pymiele==0.4.3
|
||||
|
||||
# homeassistant.components.mochad
|
||||
pymochad==0.2.0
|
||||
@@ -2017,7 +2017,7 @@ python-picnic-api2==1.2.4
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.16.1
|
||||
python-roborock==2.18.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.39
|
||||
@@ -2099,7 +2099,7 @@ pywilight==0.0.74
|
||||
pywizlight==0.6.2
|
||||
|
||||
# homeassistant.components.wmspro
|
||||
pywmspro==0.2.1
|
||||
pywmspro==0.2.2
|
||||
|
||||
# homeassistant.components.ws66i
|
||||
pyws66i==1.1
|
||||
@@ -2341,7 +2341,7 @@ tesla-powerwall==0.5.2
|
||||
tesla-wall-connector==1.0.2
|
||||
|
||||
# homeassistant.components.teslemetry
|
||||
teslemetry-stream==0.7.5
|
||||
teslemetry-stream==0.7.7
|
||||
|
||||
# homeassistant.components.tessie
|
||||
tessie-api==0.1.1
|
||||
@@ -2430,7 +2430,7 @@ url-normalize==2.2.1
|
||||
uvcclient==0.12.1
|
||||
|
||||
# homeassistant.components.roborock
|
||||
vacuum-map-parser-roborock==0.1.2
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
@@ -2448,7 +2448,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.1
|
||||
voip-utils==0.3.2
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.3
|
||||
@@ -2555,7 +2555,7 @@ yt-dlp[default]==2025.03.31
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.2
|
||||
|
||||
@@ -208,7 +208,6 @@ EXCEPTIONS = {
|
||||
# https://github.com/jaraco/skeleton/pull/170
|
||||
# https://github.com/jaraco/skeleton/pull/171
|
||||
"jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21
|
||||
"setuptools", # MIT
|
||||
}
|
||||
|
||||
TODO = {
|
||||
|
||||
@@ -316,65 +316,6 @@ async def test_release_slot_on_connect_exception(
|
||||
cancel_hci1()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth", "two_adapters")
|
||||
async def test_we_switch_adapters_on_failure(
|
||||
hass: HomeAssistant,
|
||||
install_bleak_catcher,
|
||||
) -> None:
|
||||
"""Ensure we try the next best adapter after a failure."""
|
||||
hci0_device_advs, cancel_hci0, cancel_hci1 = _generate_scanners_with_fake_devices(
|
||||
hass
|
||||
)
|
||||
ble_device = hci0_device_advs["00:00:00:00:00:01"][0]
|
||||
client = bleak.BleakClient(ble_device)
|
||||
|
||||
class FakeBleakClientFailsHCI0Only(BaseFakeBleakClient):
|
||||
"""Fake bleak client that fails to connect."""
|
||||
|
||||
async def connect(self, *args, **kwargs):
|
||||
"""Connect."""
|
||||
if "/hci0/" in self._device.details["path"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
with patch(
|
||||
"habluetooth.wrappers.get_platform_client_backend_type",
|
||||
return_value=FakeBleakClientFailsHCI0Only,
|
||||
):
|
||||
assert await client.connect() is False
|
||||
|
||||
with patch(
|
||||
"habluetooth.wrappers.get_platform_client_backend_type",
|
||||
return_value=FakeBleakClientFailsHCI0Only,
|
||||
):
|
||||
assert await client.connect() is False
|
||||
|
||||
# After two tries we should switch to hci1
|
||||
with patch(
|
||||
"habluetooth.wrappers.get_platform_client_backend_type",
|
||||
return_value=FakeBleakClientFailsHCI0Only,
|
||||
):
|
||||
assert await client.connect() is True
|
||||
|
||||
# ..and we remember that hci1 works as long as the client doesn't change
|
||||
with patch(
|
||||
"habluetooth.wrappers.get_platform_client_backend_type",
|
||||
return_value=FakeBleakClientFailsHCI0Only,
|
||||
):
|
||||
assert await client.connect() is True
|
||||
|
||||
# If we replace the client, we should try hci0 again
|
||||
client = bleak.BleakClient(ble_device)
|
||||
|
||||
with patch(
|
||||
"habluetooth.wrappers.get_platform_client_backend_type",
|
||||
return_value=FakeBleakClientFailsHCI0Only,
|
||||
):
|
||||
assert await client.connect() is False
|
||||
cancel_hci0()
|
||||
cancel_hci1()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth", "two_adapters")
|
||||
async def test_passing_subclassed_str_as_address(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -224,16 +224,20 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_RESOLVER: "8.8.8.8",
|
||||
CONF_RESOLVER_IPV6: "2001:4860:4860::8888",
|
||||
CONF_PORT: 53,
|
||||
CONF_PORT_IPV6: 53,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
with patch(
|
||||
"homeassistant.components.dnsip.config_flow.aiodns.DNSResolver",
|
||||
return_value=RetrieveDNS(),
|
||||
):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_RESOLVER: "8.8.8.8",
|
||||
CONF_RESOLVER_IPV6: "2001:4860:4860::8888",
|
||||
CONF_PORT: 53,
|
||||
CONF_PORT_IPV6: 53,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
|
||||
@@ -27,7 +27,7 @@ from homeassistant.components.esphome.const import (
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -747,6 +747,35 @@ async def test_discovery_already_configured(hass: HomeAssistant) -> None:
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf")
|
||||
async def test_discovery_ignored(hass: HomeAssistant) -> None:
|
||||
"""Test discovery does not probe and ignored entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "test8266.local", CONF_PORT: 6053, CONF_PASSWORD: ""},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
source=SOURCE_IGNORE,
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
service_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.43.183"),
|
||||
ip_addresses=[ip_address("192.168.43.183")],
|
||||
hostname="test8266.local.",
|
||||
name="mock_name",
|
||||
port=6053,
|
||||
properties={"mac": "1122334455aa"},
|
||||
type="mock_type",
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf")
|
||||
async def test_discovery_duplicate_data(hass: HomeAssistant) -> None:
|
||||
"""Test discovery aborts if same mDNS packet arrives."""
|
||||
@@ -786,8 +815,8 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None:
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
service_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.43.183"),
|
||||
ip_addresses=[ip_address("192.168.43.183")],
|
||||
ip_address=ip_address("192.168.43.184"),
|
||||
ip_addresses=[ip_address("192.168.43.184")],
|
||||
hostname="test8266.local.",
|
||||
name="mock_name",
|
||||
port=6053,
|
||||
@@ -806,9 +835,40 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None:
|
||||
"mac": "11:22:33:44:55:aa",
|
||||
}
|
||||
|
||||
assert entry.data[CONF_HOST] == "192.168.43.184"
|
||||
assert entry.unique_id == "11:22:33:44:55:aa"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_client", "mock_setup_entry", "mock_zeroconf")
|
||||
async def test_discovery_abort_without_update_same_host_port(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test discovery aborts without update when hsot and port are the same."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""},
|
||||
unique_id="11:22:33:44:55:aa",
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
service_info = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.168.43.183"),
|
||||
ip_addresses=[ip_address("192.168.43.183")],
|
||||
hostname="test8266.local.",
|
||||
name="mock_name",
|
||||
port=6053,
|
||||
properties={"address": "test8266.local", "mac": "1122334455aa"},
|
||||
type="mock_type",
|
||||
)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry", "mock_zeroconf")
|
||||
async def test_user_requires_psk(hass: HomeAssistant, mock_client: APIClient) -> None:
|
||||
"""Test user step with requiring encryption key."""
|
||||
|
||||
@@ -10,8 +10,11 @@ from aioesphomeapi import (
|
||||
BinarySensorState,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
build_unique_id,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.esphome import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_RESTORED,
|
||||
@@ -19,6 +22,7 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -513,3 +517,151 @@ async def test_entity_without_name_device_with_friendly_name(
|
||||
# Make sure we have set the name to `None` as otherwise
|
||||
# the friendly_name will be "The Best Mixer "
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass_storage")
|
||||
async def test_entity_id_preserved_on_upgrade(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
assert (
|
||||
build_unique_id("11:22:33:44:55:AA", entity_info[0])
|
||||
== "11:22:33:44:55:AA-binary_sensor-my"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
suggested_object_id="should_not_change",
|
||||
)
|
||||
assert entry.entity_id == "binary_sensor.should_not_change"
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.should_not_change")
|
||||
assert state is not None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass_storage")
|
||||
async def test_entity_id_preserved_on_upgrade_old_format_entity_id(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade from old format."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
assert (
|
||||
build_unique_id("11:22:33:44:55:AA", entity_info[0])
|
||||
== "11:22:33:44:55:AA-binary_sensor-my"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
suggested_object_id="my",
|
||||
)
|
||||
assert entry.entity_id == "binary_sensor.my"
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.my")
|
||||
assert state is not None
|
||||
|
||||
|
||||
async def test_entity_id_preserved_on_upgrade_when_in_storage(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
hass_storage: dict[str, Any],
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade with user defined entity_id."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.mixer_my")
|
||||
assert state is not None
|
||||
# now rename the entity
|
||||
ent_reg_entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
ent_reg_entry.entity_id,
|
||||
new_entity_id="binary_sensor.user_named",
|
||||
)
|
||||
await hass.config_entries.async_unload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entry = device.entry
|
||||
entry_id = entry.entry_id
|
||||
storage_key = f"esphome.{entry_id}"
|
||||
assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1
|
||||
binary_sensor_data: dict[str, Any] = hass_storage[storage_key]["data"][
|
||||
"binary_sensor"
|
||||
][0]
|
||||
assert binary_sensor_data["name"] == "my"
|
||||
assert binary_sensor_data["object_id"] == "my"
|
||||
device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
entry=entry,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.user_named")
|
||||
assert state is not None
|
||||
|
||||
@@ -211,6 +211,8 @@ async def test_set_temperature(
|
||||
) -> None:
|
||||
"""Test setting temperature."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -288,6 +290,8 @@ async def test_set_hvac_mode(
|
||||
) -> None:
|
||||
"""Test setting hvac mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.target_temperature = target_temperature
|
||||
|
||||
if current_preset is PRESET_COMFORT:
|
||||
@@ -335,6 +339,8 @@ async def test_set_preset_mode_comfort(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.comfort_temperature = comfort_temperature
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
@@ -366,6 +372,8 @@ async def test_set_preset_mode_eco(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.eco_temperature = eco_temperature
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
@@ -387,6 +395,8 @@ async def test_set_preset_mode_boost(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -471,11 +481,106 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
assert state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_data",
|
||||
[
|
||||
{ATTR_TEMPERATURE: 23},
|
||||
{
|
||||
ATTR_HVAC_MODE: HVACMode.HEAT,
|
||||
ATTR_TEMPERATURE: 25,
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_set_temperature_lock(
|
||||
hass: HomeAssistant,
|
||||
fritz: Mock,
|
||||
service_data: dict,
|
||||
) -> None:
|
||||
"""Test setting temperature while device is locked."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = True
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, **service_data},
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "target_temperature", "current_preset", "expected_call_args"),
|
||||
[
|
||||
# mode off always sets target temperature to 0
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 22, PRESET_COMFORT, [call(0, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 16, PRESET_ECO, [call(0, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 16, None, [call(0, True)]),
|
||||
# mode heat sets target temperature based on current scheduled preset,
|
||||
# when not already in mode heat
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_COMFORT, [call(22, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_ECO, [call(16, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, None, [call(22, True)]),
|
||||
# mode heat does not set target temperature, when already in mode heat
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_COMFORT, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_ECO, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, None, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_COMFORT, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_ECO, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, None, []),
|
||||
],
|
||||
)
|
||||
async def test_set_hvac_mode_lock(
|
||||
hass: HomeAssistant,
|
||||
fritz: Mock,
|
||||
service_data: dict,
|
||||
target_temperature: float,
|
||||
current_preset: str,
|
||||
expected_call_args: list[_Call],
|
||||
) -> None:
|
||||
"""Test setting hvac mode while device is locked."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = True
|
||||
device.target_temperature = target_temperature
|
||||
|
||||
if current_preset is PRESET_COMFORT:
|
||||
device.nextchange_temperature = device.eco_temperature
|
||||
elif current_preset is PRESET_ECO:
|
||||
device.nextchange_temperature = device.comfort_temperature
|
||||
else:
|
||||
device.nextchange_endperiod = 0
|
||||
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, **service_data},
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
async def test_holidy_summer_mode(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock
|
||||
) -> None:
|
||||
"""Test holiday and summer mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -510,7 +615,7 @@ async def test_holidy_summer_mode(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change HVAC mode while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -520,7 +625,7 @@ async def test_holidy_summer_mode(
|
||||
)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change preset while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -546,7 +651,7 @@ async def test_holidy_summer_mode(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change HVAC mode while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -556,7 +661,7 @@ async def test_holidy_summer_mode(
|
||||
)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change preset while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
|
||||
@@ -269,6 +269,49 @@ async def test_ingress_request_options(
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"build_type",
|
||||
[
|
||||
("a3_vl", "test/beer/ping?index=1"),
|
||||
("core", "index.html"),
|
||||
("local", "panel/config"),
|
||||
("jk_921", "editor.php?idx=3&ping=5"),
|
||||
("fsadjf10312", ""),
|
||||
],
|
||||
)
|
||||
async def test_ingress_request_head(
|
||||
hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test no auth needed for ."""
|
||||
aioclient_mock.head(
|
||||
f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}",
|
||||
text="test",
|
||||
)
|
||||
|
||||
resp = await hassio_noauth_client.head(
|
||||
f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}",
|
||||
headers={"X-Test-Header": "beer"},
|
||||
)
|
||||
|
||||
# Check we got right response
|
||||
assert resp.status == HTTPStatus.OK
|
||||
body = await resp.text()
|
||||
assert body == "" # head does not return a body
|
||||
|
||||
# Check we forwarded command
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3]
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress"
|
||||
assert (
|
||||
aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"]
|
||||
== f"/api/hassio_ingress/{build_type[0]}"
|
||||
)
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"build_type",
|
||||
[
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homematicip.auth import Auth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.connection.connection_context import ConnectionContext
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.connection.connection_context import ConnectionContext
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
from homeassistant.components.homematicip_cloud.const import (
|
||||
CONF_ACCESSPOINT,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import SERVICE_OPEN_COVER
|
||||
from homeassistant.components.lock import SERVICE_LOCK
|
||||
from homeassistant.components.button import SERVICE_PRESS
|
||||
from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER
|
||||
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@@ -121,41 +123,130 @@ async def test_turn_on_intent(hass: HomeAssistant) -> None:
|
||||
assert call.data == {"entity_id": ["light.test_light"]}
|
||||
|
||||
|
||||
async def test_translated_turn_on_intent(
|
||||
@pytest.mark.parametrize("domain", ["button", "input_button"])
|
||||
async def test_turn_on_intent_button(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, domain
|
||||
) -> None:
|
||||
"""Test HassTurnOn intent on button domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
button = entity_registry.async_get_or_create(domain, "test", "button_uid")
|
||||
|
||||
hass.states.async_set(button.entity_id, "unknown")
|
||||
button_service_calls = async_mock_service(hass, domain, SERVICE_PRESS)
|
||||
|
||||
with pytest.raises(intent.IntentHandleError):
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": button.entity_id}}
|
||||
)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": button.entity_id}}
|
||||
)
|
||||
|
||||
assert len(button_service_calls) == 1
|
||||
call = button_service_calls[0]
|
||||
assert call.domain == domain
|
||||
assert call.service == SERVICE_PRESS
|
||||
assert call.data == {"entity_id": button.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_valve(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn intent on domains which don't have the intent."""
|
||||
result = await async_setup_component(hass, "homeassistant", {})
|
||||
result = await async_setup_component(hass, "intent", {})
|
||||
await hass.async_block_till_done()
|
||||
assert result
|
||||
"""Test HassTurnOn/Off intent on valve domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
valve = entity_registry.async_get_or_create("valve", "test", "valve_uid")
|
||||
|
||||
hass.states.async_set(valve.entity_id, "closed")
|
||||
open_calls = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE)
|
||||
close_calls = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": valve.entity_id}}
|
||||
)
|
||||
|
||||
assert len(open_calls) == 1
|
||||
call = open_calls[0]
|
||||
assert call.domain == "valve"
|
||||
assert call.service == SERVICE_OPEN_VALVE
|
||||
assert call.data == {"entity_id": valve.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": valve.entity_id}}
|
||||
)
|
||||
|
||||
assert len(close_calls) == 1
|
||||
call = close_calls[0]
|
||||
assert call.domain == "valve"
|
||||
assert call.service == SERVICE_CLOSE_VALVE
|
||||
assert call.data == {"entity_id": valve.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_cover(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn/Off intent on cover domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
cover = entity_registry.async_get_or_create("cover", "test", "cover_uid")
|
||||
lock = entity_registry.async_get_or_create("lock", "test", "lock_uid")
|
||||
|
||||
hass.states.async_set(cover.entity_id, "closed")
|
||||
hass.states.async_set(lock.entity_id, "unlocked")
|
||||
cover_service_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
|
||||
lock_service_calls = async_mock_service(hass, "lock", SERVICE_LOCK)
|
||||
open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
|
||||
close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": cover.entity_id}}
|
||||
)
|
||||
|
||||
assert len(open_calls) == 1
|
||||
call = open_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == SERVICE_OPEN_COVER
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": cover.entity_id}}
|
||||
)
|
||||
|
||||
assert len(close_calls) == 1
|
||||
call = close_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == SERVICE_CLOSE_COVER
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_lock(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn/Off intent on lock domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
lock = entity_registry.async_get_or_create("lock", "test", "lock_uid")
|
||||
|
||||
hass.states.async_set(lock.entity_id, "locked")
|
||||
unlock_calls = async_mock_service(hass, "lock", SERVICE_UNLOCK)
|
||||
lock_calls = async_mock_service(hass, "lock", SERVICE_LOCK)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": lock.entity_id}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(cover_service_calls) == 1
|
||||
call = cover_service_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == "open_cover"
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
assert len(lock_service_calls) == 1
|
||||
call = lock_service_calls[0]
|
||||
assert len(lock_calls) == 1
|
||||
call = lock_calls[0]
|
||||
assert call.domain == "lock"
|
||||
assert call.service == "lock"
|
||||
assert call.service == SERVICE_LOCK
|
||||
assert call.data == {"entity_id": lock.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": lock.entity_id}}
|
||||
)
|
||||
|
||||
assert len(unlock_calls) == 1
|
||||
call = unlock_calls[0]
|
||||
assert call.domain == "lock"
|
||||
assert call.service == SERVICE_UNLOCK
|
||||
assert call.data == {"entity_id": lock.entity_id}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
"""Tests for La Marzocco binary sensors."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -33,6 +34,16 @@ async def test_binary_sensors(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_websocket_terminated() -> Generator[bool]:
|
||||
"""Mock websocket terminated."""
|
||||
with patch(
|
||||
"homeassistant.components.lamarzocco.coordinator.LaMarzoccoUpdateCoordinator.websocket_terminated",
|
||||
new=False,
|
||||
) as mock_websocket_terminated:
|
||||
yield mock_websocket_terminated
|
||||
|
||||
|
||||
async def test_brew_active_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
|
||||
@@ -958,21 +958,6 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None:
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data["brightness"] == 40 # 50 - 10
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [entity0.entity_id, entity1.entity_id],
|
||||
"brightness_step_pct": 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
_, data = entity0.last_call("turn_on")
|
||||
assert data["brightness"] == 116 # 90 + (255 * 0.10)
|
||||
_, data = entity1.last_call("turn_on")
|
||||
assert data["brightness"] == 66 # 40 + (255 * 0.10)
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
@@ -983,7 +968,49 @@ async def test_light_brightness_step(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off
|
||||
assert entity0.state == "off" # 40 - 126; brightness is 0, light should turn off
|
||||
|
||||
|
||||
async def test_light_brightness_step_pct(hass: HomeAssistant) -> None:
|
||||
"""Test that percentage based brightness steps work as expected."""
|
||||
entity = MockLight("Test_0", STATE_ON)
|
||||
|
||||
setup_test_component_platform(hass, light.DOMAIN, [entity])
|
||||
|
||||
entity.supported_features = light.SUPPORT_BRIGHTNESS
|
||||
# Set color modes to none to trigger backwards compatibility in LightEntity
|
||||
entity.supported_color_modes = None
|
||||
entity.color_mode = None
|
||||
entity.brightness = 255
|
||||
assert await async_setup_component(hass, "light", {"light": {"platform": "test"}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity.entity_id)
|
||||
assert state is not None
|
||||
assert state.attributes["brightness"] == 255 # 100%
|
||||
|
||||
def reduce_brightness_by_ten_percent():
|
||||
return hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": [entity.entity_id],
|
||||
"brightness_step_pct": -10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await reduce_brightness_by_ten_percent()
|
||||
_, data = entity.last_call("turn_on")
|
||||
assert round(data["brightness"] / 2.55) == 90 # 100% - 10% = 90%
|
||||
|
||||
await reduce_brightness_by_ten_percent()
|
||||
_, data = entity.last_call("turn_on")
|
||||
assert round(data["brightness"] / 2.55) == 80 # 90% - 10% = 80%
|
||||
|
||||
await reduce_brightness_by_ten_percent()
|
||||
_, data = entity.last_call("turn_on")
|
||||
assert round(data["brightness"] / 2.55) == 70 # 80% - 10% = 70%
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_custom_integrations")
|
||||
|
||||
@@ -210,5 +210,129 @@
|
||||
"ecoFeedback": null,
|
||||
"batteryLevel": null
|
||||
}
|
||||
},
|
||||
"DummyAppliance_74_off": {
|
||||
"ident": {
|
||||
"type": {
|
||||
"key_localized": "Device type",
|
||||
"value_raw": 74,
|
||||
"value_localized": "Hob with vapour extraction"
|
||||
},
|
||||
"deviceName": "",
|
||||
"protocolVersion": 2,
|
||||
"deviceIdentLabel": {
|
||||
"fabNumber": "**REDACTED**",
|
||||
"fabIndex": "00",
|
||||
"techType": "KMDA7473",
|
||||
"matNumber": "",
|
||||
"swids": ["000"]
|
||||
},
|
||||
"xkmIdentLabel": {
|
||||
"techType": "EK039W",
|
||||
"releaseVersion": "02.80"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"ProgramID": {
|
||||
"value_raw": 0,
|
||||
"value_localized": "",
|
||||
"key_localized": "Program name"
|
||||
},
|
||||
"status": {
|
||||
"value_raw": 1,
|
||||
"value_localized": "Off",
|
||||
"key_localized": "status"
|
||||
},
|
||||
"programType": {
|
||||
"value_raw": 0,
|
||||
"value_localized": "",
|
||||
"key_localized": "Program type"
|
||||
},
|
||||
"programPhase": {
|
||||
"value_raw": 0,
|
||||
"value_localized": "",
|
||||
"key_localized": "Program phase"
|
||||
},
|
||||
"remainingTime": [0, 0],
|
||||
"startTime": [0, 0],
|
||||
"targetTemperature": [
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
},
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
},
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
}
|
||||
],
|
||||
"coreTargetTemperature": [
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
}
|
||||
],
|
||||
"temperature": [
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
},
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
},
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
}
|
||||
],
|
||||
"coreTemperature": [
|
||||
{
|
||||
"value_raw": -32768,
|
||||
"value_localized": null,
|
||||
"unit": "Celsius"
|
||||
}
|
||||
],
|
||||
"signalInfo": false,
|
||||
"signalFailure": false,
|
||||
"signalDoor": false,
|
||||
"remoteEnable": {
|
||||
"fullRemoteControl": false,
|
||||
"smartGrid": false,
|
||||
"mobileStart": false
|
||||
},
|
||||
"ambientLight": null,
|
||||
"light": null,
|
||||
"elapsedTime": [],
|
||||
"spinningSpeed": {
|
||||
"unit": "rpm",
|
||||
"value_raw": null,
|
||||
"value_localized": null,
|
||||
"key_localized": "Spin speed"
|
||||
},
|
||||
"dryingStep": {
|
||||
"value_raw": null,
|
||||
"value_localized": "",
|
||||
"key_localized": "Drying level"
|
||||
},
|
||||
"ventilationStep": {
|
||||
"value_raw": null,
|
||||
"value_localized": "",
|
||||
"key_localized": "Fan level"
|
||||
},
|
||||
"plateStep": [],
|
||||
"ecoFeedback": null,
|
||||
"batteryLevel": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-entry]
|
||||
# name: test_climate_states[platforms0-freezer][climate.freezer-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -19,7 +19,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.freezer_freezer',
|
||||
'entity_id': 'climate.freezer',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -31,7 +31,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Freezer',
|
||||
'original_name': None,
|
||||
'platform': 'miele',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
@@ -40,11 +40,11 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_climate_states[platforms0-freezer][climate.freezer_freezer-state]
|
||||
# name: test_climate_states[platforms0-freezer][climate.freezer-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': -18,
|
||||
'friendly_name': 'Freezer Freezer',
|
||||
'friendly_name': 'Freezer',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
]),
|
||||
@@ -55,14 +55,14 @@
|
||||
'temperature': -18,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.freezer_freezer',
|
||||
'entity_id': 'climate.freezer',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'cool',
|
||||
})
|
||||
# ---
|
||||
# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-entry]
|
||||
# name: test_climate_states[platforms0-freezer][climate.refrigerator-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -82,7 +82,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.refrigerator_refrigerator',
|
||||
'entity_id': 'climate.refrigerator',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@@ -94,7 +94,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Refrigerator',
|
||||
'original_name': None,
|
||||
'platform': 'miele',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 1>,
|
||||
@@ -103,11 +103,11 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_climate_states[platforms0-freezer][climate.refrigerator_refrigerator-state]
|
||||
# name: test_climate_states[platforms0-freezer][climate.refrigerator-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 4,
|
||||
'friendly_name': 'Refrigerator Refrigerator',
|
||||
'friendly_name': 'Refrigerator',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
]),
|
||||
@@ -118,7 +118,7 @@
|
||||
'temperature': 4,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.refrigerator_refrigerator',
|
||||
'entity_id': 'climate.refrigerator',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
@@ -48,6 +48,55 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'fan',
|
||||
'entity_category': None,
|
||||
'entity_id': 'fan.hob_with_extraction_fan_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Fan',
|
||||
'platform': 'miele',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'fan',
|
||||
'unique_id': 'DummyAppliance_74_off-fan_readonly',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_states[fan_devices.json-platforms0][fan.hob_with_extraction_fan_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Hob with extraction Fan',
|
||||
'supported_features': <FanEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'fan.hob_with_extraction_fan_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_fan_states[fan_devices.json-platforms0][fan.hood_fan-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -26,7 +26,7 @@ pytestmark = [
|
||||
),
|
||||
]
|
||||
|
||||
ENTITY_ID = "climate.freezer_freezer"
|
||||
ENTITY_ID = "climate.freezer"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = {
|
||||
MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
|
||||
"5269352dd9534c908d22812ea5d714cd": {
|
||||
"platform": "notify",
|
||||
"name": None,
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value }}",
|
||||
"entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user