This commit is contained in:
Franck Nijhof 2024-06-11 14:41:12 +02:00 committed by GitHub
commit 090d296135
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1054 additions and 325 deletions

View File

@ -163,7 +163,6 @@ homeassistant.components.easyenergy.*
homeassistant.components.ecovacs.*
homeassistant.components.ecowitt.*
homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*

View File

@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor
/tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @bdraco
/tests/components/unifiprotect/ @bdraco
/homeassistant/components/upb/ @gwww
/tests/components/upb/ @gwww
/homeassistant/components/upc_connect/ @pvizeli @fabaff

View File

@ -2,5 +2,5 @@
DOMAIN = "aladdin_connect"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"

View File

@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
Adds an empty filter to hass data.
Tries to get a filter from yaml, if present set to hass data.
If config is empty after getting the filter, return, otherwise emit
deprecated warning and pass the rest to the config flow.
"""
hass.data.setdefault(DOMAIN, {DATA_FILTER: {}})
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
if DOMAIN in yaml_config:
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER]
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
return True
@ -207,6 +206,6 @@ class AzureDataExplorer:
if "\n" in state.state:
return None, dropped + 1
json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
json_event = json.dumps(obj=state, cls=JSONEncoder)
return (json_event, dropped)

View File

@ -23,7 +23,7 @@ from .const import (
CONF_APP_REG_ID,
CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID,
CONF_USE_FREE,
CONF_USE_QUEUED_CLIENT,
)
_LOGGER = logging.getLogger(__name__)
@ -35,7 +35,6 @@ class AzureDataExplorerClient:
def __init__(self, data: Mapping[str, Any]) -> None:
"""Create the right class."""
self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI]
self._database = data[CONF_ADX_DATABASE_NAME]
self._table = data[CONF_ADX_TABLE_NAME]
self._ingestion_properties = IngestionProperties(
@ -45,24 +44,36 @@ class AzureDataExplorerClient:
ingestion_mapping_reference="ha_json_mapping",
)
# Create cLient for ingesting and querying data
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
self._cluster_ingest_uri,
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
# Create client for ingesting data
kcsb_ingest = (
KustoConnectionStringBuilder.with_aad_application_key_authentication(
data[CONF_ADX_CLUSTER_INGEST_URI],
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
)
)
if data[CONF_USE_FREE] is True:
# Queded is the only option supported on free tear of ADX
self.write_client = QueuedIngestClient(kcsb)
else:
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb)
# Create client for querying data
kcsb_query = (
KustoConnectionStringBuilder.with_aad_application_key_authentication(
data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""),
data[CONF_APP_REG_ID],
data[CONF_APP_REG_SECRET],
data[CONF_AUTHORITY_ID],
)
)
self.query_client = KustoClient(kcsb)
if data[CONF_USE_QUEUED_CLIENT] is True:
# Queded is the only option supported on free tear of ADX
self.write_client = QueuedIngestClient(kcsb_ingest)
else:
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
self.query_client = KustoClient(kcsb_query)
def test_connection(self) -> None:
"""Test connection, will throw Exception when it cannot connect."""
"""Test connection, will throw Exception if it cannot connect."""
query = f"{self._table} | take 1"

View File

@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlowResult
from homeassistant.helpers.selector import BooleanSelector
from . import AzureDataExplorerClient
from .const import (
@ -19,7 +20,7 @@ from .const import (
CONF_APP_REG_ID,
CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID,
CONF_USE_FREE,
CONF_USE_QUEUED_CLIENT,
DEFAULT_OPTIONS,
DOMAIN,
)
@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_APP_REG_ID): str,
vol.Required(CONF_APP_REG_SECRET): str,
vol.Required(CONF_AUTHORITY_ID): str,
vol.Optional(CONF_USE_FREE, default=False): bool,
vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(),
}
)

View File

@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id"
CONF_SEND_INTERVAL = "send_interval"
CONF_MAX_DELAY = "max_delay"
CONF_FILTER = DATA_FILTER = "filter"
CONF_USE_FREE = "use_queued_ingestion"
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
DATA_HUB = "hub"
STEP_USER = "user"

View File

@ -3,15 +3,19 @@
"step": {
"user": {
"title": "Setup your Azure Data Explorer integration",
"description": "Enter connection details.",
"description": "Enter connection details",
"data": {
"cluster_ingest_uri": "Cluster ingest URI",
"database": "Database name",
"table": "Table name",
"cluster_ingest_uri": "Cluster Ingest URI",
"authority_id": "Authority ID",
"client_id": "Client ID",
"client_secret": "Client secret",
"authority_id": "Authority ID",
"database": "Database name",
"table": "Table name",
"use_queued_ingestion": "Use queued ingestion"
},
"data_description": {
"cluster_ingest_uri": "Ingest-URI of the cluster",
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
}
}
},

View File

@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
director_all_items = json.loads(director_all_items)
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration())
# Check if OS version is 3 or higher to get UI configuration
entry_data[CONF_UI_CONFIGURATION] = None
if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3:
entry_data[CONF_UI_CONFIGURATION] = json.loads(
await director.getUiConfiguration()
)
# Load options from config entry
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(

View File

@ -81,11 +81,18 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Control4 rooms from a config entry."""
entry_data = hass.data[DOMAIN][entry.entry_id]
ui_config = entry_data[CONF_UI_CONFIGURATION]
# OS 2 will not have a ui_configuration
if not ui_config:
_LOGGER.debug("No UI Configuration found for Control4")
return
all_rooms = await get_rooms(hass, entry)
if not all_rooms:
return
entry_data = hass.data[DOMAIN][entry.entry_id]
scan_interval = entry_data[CONF_SCAN_INTERVAL]
_LOGGER.debug("Scan interval = %s", scan_interval)
@ -119,8 +126,6 @@ async def async_setup_entry(
if "parentId" in item and k > 1
}
ui_config = entry_data[CONF_UI_CONFIGURATION]
entity_list = []
for room in all_rooms:
room_id = room["id"]

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
"iot_class": "cloud_polling",
"requirements": ["pyElectra==1.2.0"]
"requirements": ["pyElectra==1.2.1"]
}

View File

@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity):
self._attr_unique_id = coordinator.data.info.serial_number
# Elgato Light supporting color, have a different temperature range
if self.coordinator.data.settings.power_on_hue is not None:
if (
self.coordinator.data.info.product_name
in (
"Elgato Light Strip",
"Elgato Light Strip Pro",
)
or self.coordinator.data.settings.power_on_hue
or self.coordinator.data.state.hue is not None
):
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = 153
self._attr_max_mireds = 285

View File

@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity):
self._device: OpenWebIfDevice = device
self._entry = entry
self._attr_unique_id = device.mac_address
self._attr_unique_id = device.mac_address or entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.mac_address)},
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=about["info"]["brand"],
model=about["info"]["model"],
configuration_url=device.base,

View File

@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
):
"""Initialize the alarm panel."""
self._partition_number = partition_number
self._code = code
self._panic_type = panic_type
self._alarm_control_panel_option_default_code = code
self._attr_code_format = CodeFormat.NUMBER
_LOGGER.debug("Setting up alarm: %s", alarm_name)
super().__init__(alarm_name, info, controller)
@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
if partition is None or int(partition) == self._partition_number:
self.async_write_ha_state()
@property
def code_format(self) -> CodeFormat | None:
"""Regex for code format or None if no code is required."""
if self._code:
return None
return CodeFormat.NUMBER
@property
def state(self) -> str:
"""Return the state of the device."""
@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
if code:
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
else:
self.hass.data[DATA_EVL].disarm_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
if code:
self.hass.data[DATA_EVL].arm_stay_partition(
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_stay_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
if code:
self.hass.data[DATA_EVL].arm_away_partition(
str(code), self._partition_number
)
else:
self.hass.data[DATA_EVL].arm_away_partition(
str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number)
async def async_alarm_trigger(self, code: str | None = None) -> None:
"""Alarm trigger command. Will be used to trigger a panic alarm."""
@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
self.hass.data[DATA_EVL].arm_night_partition(
str(code) if code else str(self._code), self._partition_number
)
self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number)
@callback
def async_alarm_keypress(self, keypress=None):

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240605.0"]
"requirements": ["home-assistant-frontend==20240610.0"]
}

View File

@ -13,5 +13,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling",
"requirements": ["gardena-bluetooth==1.4.1"]
"requirements": ["gardena-bluetooth==1.4.2"]
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/glances",
"iot_class": "local_polling",
"loggers": ["glances_api"],
"requirements": ["glances-api==0.7.0"]
"requirements": ["glances-api==0.8.0"]
}

View File

@ -1586,6 +1586,17 @@ class ArmDisArmTrait(_Trait):
if features & required_feature != 0
]
def _default_arm_state(self):
states = self._supported_states()
if STATE_ALARM_TRIGGERED in states:
states.remove(STATE_ALARM_TRIGGERED)
if len(states) != 1:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
return states[0]
def sync_attributes(self):
"""Return ArmDisarm attributes for a sync request."""
response = {}
@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait):
def query_attributes(self):
"""Return ArmDisarm query attributes."""
armed_state = self.state.attributes.get("next_state", self.state.state)
response = {"isArmed": armed_state in self.state_to_service}
if response["isArmed"]:
response.update({"currentArmLevel": armed_state})
return response
if armed_state in self.state_to_service:
return {"isArmed": True, "currentArmLevel": armed_state}
return {
"isArmed": False,
"currentArmLevel": self._default_arm_state(),
}
async def execute(self, command, data, params, challenge):
"""Execute an ArmDisarm command."""
@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait):
# If no arm level given, we can only arm it if there is
# only one supported arm type. We never default to triggered.
if not (arm_level := params.get("armLevel")):
states = self._supported_states()
if STATE_ALARM_TRIGGERED in states:
states.remove(STATE_ALARM_TRIGGERED)
if len(states) != 1:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
arm_level = states[0]
arm_level = self._default_arm_state()
if self.state.state == arm_level:
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")

View File

@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
try:
response = await model.generate_content_async(prompt_parts)
except (
ClientError,
GoogleAPICallError,
ValueError,
genai_types.BlockedPromptException,
genai_types.StopCandidateException,

View File

@ -2,11 +2,12 @@
from __future__ import annotations
import codecs
from typing import Any, Literal
import google.ai.generativelanguage as glm
from google.api_core.exceptions import GoogleAPICallError
import google.generativeai as genai
from google.generativeai import protos
import google.generativeai.types as genai_types
from google.protobuf.json_format import MessageToDict
import voluptuous as vol
@ -93,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
parameters = _format_schema(convert(tool.parameters))
return glm.Tool(
return protos.Tool(
{
"function_declarations": [
{
@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
)
def _adjust_value(value: Any) -> Any:
"""Reverse unnecessary single quotes escaping."""
def _escape_decode(value: Any) -> Any:
"""Recursively call codecs.escape_decode on all values."""
if isinstance(value, str):
return value.replace("\\'", "'")
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
if isinstance(value, list):
return [_adjust_value(item) for item in value]
return [_escape_decode(item) for item in value]
if isinstance(value, dict):
return {k: _adjust_value(v) for k, v in value.items()}
return {k: _escape_decode(v) for k, v in value.items()}
return value
@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity(
for function_call in function_calls:
tool_call = MessageToDict(function_call._pb) # noqa: SLF001
tool_name = tool_call["name"]
tool_args = {
key: _adjust_value(value)
for key, value in tool_call["args"].items()
}
tool_args = _escape_decode(tool_call["args"])
LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args)
tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
try:
@ -349,13 +347,13 @@ class GoogleGenerativeAIConversationEntity(
LOGGER.debug("Tool response: %s", function_response)
tool_responses.append(
glm.Part(
function_response=glm.FunctionResponse(
protos.Part(
function_response=protos.FunctionResponse(
name=tool_name, response=function_response
)
)
)
chat_request = glm.Content(parts=tool_responses)
chat_request = protos.Content(parts=tool_responses)
intent_response.async_set_speech(
" ".join([part.text.strip() for part in chat_response.parts if part.text])

View File

@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"]
"requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"]
}

View File

@ -36,7 +36,14 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import (
@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
get_unit_of_measurement,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity):
self._native_unit_of_measurement = unit_of_measurement
self._valid_units: set[str | None] = set()
self._can_convert: bool = False
self.calculate_attributes_later: CALLBACK_TYPE | None = None
self._attr_name = name
if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
@ -345,13 +354,32 @@ class SensorGroup(GroupEntity, SensorEntity):
async def async_added_to_hass(self) -> None:
"""When added to hass."""
for entity_id in self._entity_ids:
if self.hass.states.get(entity_id) is None:
self.calculate_attributes_later = async_track_state_change_event(
self.hass, self._entity_ids, self.calculate_state_attributes
)
break
if not self.calculate_attributes_later:
await self.calculate_state_attributes()
await super().async_added_to_hass()
async def calculate_state_attributes(
self, event: Event[EventStateChangedData] | None = None
) -> None:
"""Calculate state attributes."""
for entity_id in self._entity_ids:
if self.hass.states.get(entity_id) is None:
return
if self.calculate_attributes_later:
self.calculate_attributes_later()
self.calculate_attributes_later = None
self._attr_state_class = self._calculate_state_class(self._state_class)
self._attr_device_class = self._calculate_device_class(self._device_class)
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
self._native_unit_of_measurement
)
self._valid_units = self._get_valid_units()
await super().async_added_to_hass()
@callback
def async_update_group_state(self) -> None:

View File

@ -64,7 +64,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
desk = Desk(None, monitor_height=False)
try:
await desk.connect(discovery_info.device, auto_reconnect=False)
await desk.connect(discovery_info.device, retry=False)
except AuthFailedError:
errors["base"] = "auth_failed"
except TimeoutError:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["imgw_pib==1.0.4"]
"requirements": ["imgw_pib==1.0.5"]
}

View File

@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==4.0.4"]
"requirements": ["google-nest-sdm==4.0.5"]
}

View File

@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
async def async_turn_off(self) -> None:
"""Turn off the zone."""
await self.async_set_hvac_mode(OPERATION_MODE_OFF)
await self.async_set_hvac_mode(HVACMode.OFF)
self._signal_zone_update()
async def async_turn_on(self) -> None:
"""Turn on the zone."""
await self.async_set_hvac_mode(OPERATION_MODE_AUTO)
await self.async_set_hvac_mode(HVACMode.AUTO)
self._signal_zone_update()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:

View File

@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None:
domain=DOMAIN,
issue_id=_get_issue_id(entry_id),
is_fixable=True,
is_persistent=True,
is_persistent=False,
severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
translation_key="deprecated_v25",

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.4.6"]
"requirements": ["opower==0.4.7"]
}

View File

@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
from icmplib import NameLookupError, async_ping
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import ICMP_TIMEOUT, PING_TIMEOUT
@ -58,9 +59,16 @@ class PingDataICMPLib(PingData):
timeout=ICMP_TIMEOUT,
privileged=self._privileged,
)
except NameLookupError:
except NameLookupError as err:
self.is_alive = False
return
raise UpdateFailed(f"Error resolving host: {self.ip_address}") from err
_LOGGER.debug(
"async_ping returned: reachable=%s sent=%i received=%s",
data.is_alive,
data.packets_sent,
data.packets_received,
)
self.is_alive = data.is_alive
if not self.is_alive:
@ -94,6 +102,10 @@ class PingDataSubProcess(PingData):
async def async_ping(self) -> dict[str, Any] | None:
"""Send ICMP echo request and return details if success."""
_LOGGER.debug(
"Pinging %s with: `%s`", self.ip_address, " ".join(self._ping_cmd)
)
pinger = await asyncio.create_subprocess_exec(
*self._ping_cmd,
stdin=None,
@ -140,20 +152,17 @@ class PingDataSubProcess(PingData):
if TYPE_CHECKING:
assert match is not None
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
except TimeoutError:
_LOGGER.exception(
"Timed out running command: `%s`, after: %ss",
self._ping_cmd,
self._count + PING_TIMEOUT,
)
except TimeoutError as err:
if pinger:
with suppress(TypeError):
await pinger.kill() # type: ignore[func-returns-value]
del pinger
return None
except AttributeError:
return None
raise UpdateFailed(
f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s"
) from err
except AttributeError as err:
raise UpdateFailed from err
return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
async def async_update(self) -> None:

View File

@ -1245,7 +1245,7 @@ def _first_statistic(
table: type[StatisticsBase],
metadata_id: int,
) -> datetime | None:
"""Return the data of the oldest statistic row for a given metadata id."""
"""Return the date of the oldest statistic row for a given metadata id."""
stmt = lambda_stmt(
lambda: select(table.start_ts)
.filter(table.metadata_id == metadata_id)
@ -1257,12 +1257,30 @@ def _first_statistic(
return None
def _last_statistic(
session: Session,
table: type[StatisticsBase],
metadata_id: int,
) -> datetime | None:
"""Return the date of the newest statistic row for a given metadata id."""
stmt = lambda_stmt(
lambda: select(table.start_ts)
.filter(table.metadata_id == metadata_id)
.order_by(table.start_ts.desc())
.limit(1)
)
if stats := cast(Sequence[Row], execute_stmt_lambda_element(session, stmt)):
return dt_util.utc_from_timestamp(stats[0].start_ts)
return None
def _get_oldest_sum_statistic(
session: Session,
head_start_time: datetime | None,
main_start_time: datetime | None,
tail_start_time: datetime | None,
oldest_stat: datetime | None,
oldest_5_min_stat: datetime | None,
tail_only: bool,
metadata_id: int,
) -> float | None:
@ -1307,6 +1325,15 @@ def _get_oldest_sum_statistic(
if (
head_start_time is not None
and oldest_5_min_stat is not None
and (
# If we want stats older than the short term purge window, don't lookup
# the oldest sum in the short term table, as it would be prioritized
# over older LongTermStats.
(oldest_stat is None)
or (oldest_5_min_stat < oldest_stat)
or (oldest_5_min_stat <= head_start_time)
)
and (
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, head_start_time, StatisticsShortTerm, metadata_id
@ -1477,13 +1504,16 @@ def statistic_during_period(
tail_start_time: datetime | None = None
tail_end_time: datetime | None = None
if end_time is None:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
tail_start_time = _last_statistic(session, Statistics, metadata_id)
if tail_start_time:
tail_start_time += Statistics.duration
else:
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
elif tail_only:
tail_start_time = start_time
tail_end_time = end_time
elif end_time.minute:
tail_start_time = (
start_time
if tail_only
else end_time.replace(minute=0, second=0, microsecond=0)
)
tail_start_time = end_time.replace(minute=0, second=0, microsecond=0)
tail_end_time = end_time
# Calculate the main period
@ -1518,6 +1548,7 @@ def statistic_during_period(
main_start_time,
tail_start_time,
oldest_stat,
oldest_5_min_stat,
tail_only,
metadata_id,
)

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.2.3",
"python-roborock==2.3.0",
"vacuum-map-parser-roborock==0.1.2"
]
}

View File

@ -297,16 +297,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
if version == 2:
if minor_version < 2:
# Cleanup invalid MAC addresses - see #103512
dev_reg = dr.async_get(hass)
for device in dr.async_entries_for_config_entry(
dev_reg, config_entry.entry_id
):
new_connections = device.connections.copy()
new_connections.discard((dr.CONNECTION_NETWORK_MAC, "none"))
if new_connections != device.connections:
dev_reg.async_update_device(
device.id, new_connections=new_connections
)
# Reverted due to device registry collisions - see #119082 / #119249
minor_version = 2
hass.config_entries.async_update_entry(config_entry, minor_version=2)

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import contextlib
from typing import Final
from aioshelly.block_device import BlockDevice
@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b
entry, platforms
):
if shelly_entry_data.rpc:
with contextlib.suppress(DeviceConnectionError):
# If the device is restarting or has gone offline before
# the ping/pong timeout happens, the shutdown command
# will fail, but we don't care since we are unloading
# and if we setup again, we will fix anything that is
# in an inconsistent state at that time.
await shelly_entry_data.rpc.shutdown()
await shelly_entry_data.rpc.shutdown()
return unload_ok

View File

@ -625,7 +625,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.connected: # Already connected
return
self.connected = True
await self._async_run_connected_events()
try:
await self._async_run_connected_events()
except DeviceConnectionError as err:
LOGGER.error(
"Error running connected events for device %s: %s", self.name, err
)
self.last_update_success = False
async def _async_run_connected_events(self) -> None:
"""Run connected events.
@ -699,10 +705,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.device.connected:
try:
await async_stop_scanner(self.device)
await super().shutdown()
except InvalidAuthError:
self.entry.async_start_reauth(self.hass)
return
await super().shutdown()
except DeviceConnectionError as err:
# If the device is restarting or has gone offline before
# the ping/pong timeout happens, the shutdown command
# will fail, but we don't care since we are unloading
# and if we setup again, we will fix anything that is
# in an inconsistent state at that time.
LOGGER.debug("Error during shutdown for device %s: %s", self.name, err)
return
await self._async_disconnected(False)

View File

@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
"requirements": ["aioshelly==10.0.0"],
"requirements": ["aioshelly==10.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from aiohttp import ClientTimeout
from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED
from synology_dsm.exceptions import (
SynologyDSMAPIErrorException,
@ -40,7 +41,7 @@ DEFAULT_PORT = 5000
DEFAULT_PORT_SSL = 5001
# Options
DEFAULT_SCAN_INTERVAL = 15 # min
DEFAULT_TIMEOUT = 30 # sec
DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15)
DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
ENTITY_UNIT_LOAD = "load"

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
"iot_class": "local_polling",
"loggers": ["synology_dsm"],
"requirements": ["py-synologydsm-api==2.4.2"],
"requirements": ["py-synologydsm-api==2.4.4"],
"ssdp": [
{
"manufacturer": "Synology",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["ttn_client==0.0.4"]
"requirements": ["ttn_client==1.0.0"]
}

View File

@ -6,14 +6,14 @@ from datetime import timedelta
import logging
from aiohttp.client_exceptions import ServerDisconnectedError
from pyunifiprotect.data import Bootstrap
from pyunifiprotect.data.types import FirmwareReleaseChannel
from pyunifiprotect.exceptions import ClientError, NotAuthorized
from uiprotect.data import Bootstrap
from uiprotect.data.types import FirmwareReleaseChannel
from uiprotect.exceptions import ClientError, NotAuthorized
# Import the test_util.anonymize module from the pyunifiprotect package
# Import the test_util.anonymize module from the uiprotect package
# in __init__ to ensure it gets imported in the executor since the
# diagnostics module will not be imported in the executor.
from pyunifiprotect.test_util.anonymize import anonymize_data # noqa: F401
from uiprotect.test_util.anonymize import anonymize_data # noqa: F401
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP

View File

@ -6,7 +6,7 @@ import dataclasses
import logging
from typing import Any
from pyunifiprotect.data import (
from uiprotect.data import (
NVR,
Camera,
Light,
@ -16,7 +16,7 @@ from pyunifiprotect.data import (
ProtectModelWithId,
Sensor,
)
from pyunifiprotect.data.nvr import UOSDisk
from uiprotect.data.nvr import UOSDisk
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,

View File

@ -6,7 +6,7 @@ from dataclasses import dataclass
import logging
from typing import Final
from pyunifiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId
from uiprotect.data import ProtectAdoptableDeviceModel, ProtectModelWithId
from homeassistant.components.button import (
ButtonDeviceClass,

View File

@ -6,7 +6,7 @@ from collections.abc import Generator
import logging
from typing import Any, cast
from pyunifiprotect.data import (
from uiprotect.data import (
Camera as UFPCamera,
CameraChannel,
ModelType,

View File

@ -8,9 +8,9 @@ from pathlib import Path
from typing import Any
from aiohttp import CookieJar
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import NVR
from pyunifiprotect.exceptions import ClientError, NotAuthorized
from uiprotect import ProtectApiClient
from uiprotect.data import NVR
from uiprotect.exceptions import ClientError, NotAuthorized
from unifi_discovery import async_console_is_alive
import voluptuous as vol

View File

@ -1,6 +1,6 @@
"""Constant definitions for UniFi Protect Integration."""
from pyunifiprotect.data import ModelType, Version
from uiprotect.data import ModelType, Version
from homeassistant.const import Platform

View File

@ -8,8 +8,8 @@ from functools import partial
import logging
from typing import Any, cast
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import (
from uiprotect import ProtectApiClient
from uiprotect.data import (
NVR,
Bootstrap,
Camera,
@ -20,8 +20,8 @@ from pyunifiprotect.data import (
ProtectAdoptableDeviceModel,
WSSubscriptionMessage,
)
from pyunifiprotect.exceptions import ClientError, NotAuthorized
from pyunifiprotect.utils import log_event
from uiprotect.exceptions import ClientError, NotAuthorized
from uiprotect.utils import log_event
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any, cast
from pyunifiprotect.test_util.anonymize import anonymize_data
from uiprotect.test_util.anonymize import anonymize_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

View File

@ -6,7 +6,7 @@ from collections.abc import Callable, Sequence
import logging
from typing import TYPE_CHECKING, Any
from pyunifiprotect.data import (
from uiprotect.data import (
NVR,
Camera,
Chime,

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from typing import Any
from pyunifiprotect.data import (
from uiprotect.data import (
Light,
ModelType,
ProtectAdoptableDeviceModel,

View File

@ -5,7 +5,7 @@ from __future__ import annotations
import logging
from typing import Any, cast
from pyunifiprotect.data import (
from uiprotect.data import (
Doorlock,
LockStatusType,
ModelType,

View File

@ -1,7 +1,7 @@
{
"domain": "unifiprotect",
"name": "UniFi Protect",
"codeowners": ["@bdraco"],
"codeowners": [],
"config_flow": true,
"dependencies": ["http", "repairs"],
"dhcp": [
@ -39,9 +39,8 @@
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyunifiprotect", "unifi_discovery"],
"quality_scale": "platinum",
"requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"],
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==0.4.1", "unifi-discovery==1.1.8"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",

View File

@ -5,14 +5,14 @@ from __future__ import annotations
import logging
from typing import Any, cast
from pyunifiprotect.data import (
from uiprotect.data import (
Camera,
ModelType,
ProtectAdoptableDeviceModel,
ProtectModelWithId,
StateType,
)
from pyunifiprotect.exceptions import StreamError
from uiprotect.exceptions import StreamError
from homeassistant.components import media_source
from homeassistant.components.media_player import (

View File

@ -7,15 +7,9 @@ from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, NoReturn, cast
from pyunifiprotect.data import (
Camera,
Event,
EventType,
ModelType,
SmartDetectObjectType,
)
from pyunifiprotect.exceptions import NvrError
from pyunifiprotect.utils import from_js_time
from uiprotect.data import Camera, Event, EventType, ModelType, SmartDetectObjectType
from uiprotect.exceptions import NvrError
from uiprotect.utils import from_js_time
from yarl import URL
from homeassistant.components.camera import CameraImageView

View File

@ -6,8 +6,8 @@ from itertools import chain
import logging
from typing import TypedDict
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import Bootstrap
from uiprotect import ProtectApiClient
from uiprotect.data import Bootstrap
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity

View File

@ -8,7 +8,7 @@ from enum import Enum
import logging
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from uiprotect.data import NVR, Event, ProtectAdoptableDeviceModel
from homeassistant.helpers.entity import EntityDescription

View File

@ -7,7 +7,7 @@ from datetime import timedelta
import logging
from typing import Any
from pyunifiprotect.data import (
from uiprotect.data import (
Camera,
Doorlock,
Light,

View File

@ -4,9 +4,9 @@ from __future__ import annotations
from typing import cast
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import Bootstrap, Camera, ModelType
from pyunifiprotect.data.types import FirmwareReleaseChannel
from uiprotect import ProtectApiClient
from uiprotect.data import Bootstrap, Camera, ModelType
from uiprotect.data.types import FirmwareReleaseChannel
import voluptuous as vol
from homeassistant import data_entry_flow

View File

@ -8,8 +8,8 @@ from enum import Enum
import logging
from typing import Any, Final
from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import (
from uiprotect.api import ProtectApiClient
from uiprotect.data import (
Camera,
ChimeType,
DoorbellMessageType,

View File

@ -7,7 +7,7 @@ from datetime import datetime
import logging
from typing import Any, cast
from pyunifiprotect.data import (
from uiprotect.data import (
NVR,
Camera,
Light,

View File

@ -7,9 +7,9 @@ import functools
from typing import Any, cast
from pydantic import ValidationError
from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import Camera, Chime
from pyunifiprotect.exceptions import ClientError
from uiprotect.api import ProtectApiClient
from uiprotect.data import Camera, Chime
from uiprotect.exceptions import ClientError
import voluptuous as vol
from homeassistant.components.binary_sensor import BinarySensorDeviceClass

View File

@ -6,7 +6,7 @@ from dataclasses import dataclass
import logging
from typing import Any
from pyunifiprotect.data import (
from uiprotect.data import (
NVR,
Camera,
ProtectAdoptableDeviceModel,

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from pyunifiprotect.data import (
from uiprotect.data import (
Camera,
DoorbellMessageType,
ProtectAdoptableDeviceModel,

View File

@ -10,8 +10,8 @@ import socket
from typing import Any
from aiohttp import CookieJar
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import (
from uiprotect import ProtectApiClient
from uiprotect.data import (
Bootstrap,
CameraChannel,
Light,

View File

@ -9,8 +9,8 @@ from typing import Any
from urllib.parse import urlencode
from aiohttp import web
from pyunifiprotect.data import Camera, Event
from pyunifiprotect.exceptions import ClientError
from uiprotect.data import Camera, Event
from uiprotect.exceptions import ClientError
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import HomeAssistant, callback

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waqi",
"iot_class": "cloud_polling",
"loggers": ["aiowaqi"],
"requirements": ["aiowaqi==3.0.1"]
"requirements": ["aiowaqi==3.1.0"]
}

View File

@ -269,7 +269,7 @@ class IsWorkdaySensor(BinarySensorEntity):
def _update_state_and_setup_listener(self) -> None:
"""Update state and setup listener for next interval."""
now = dt_util.utcnow()
now = dt_util.now()
self.update_data(now)
self.unsub = async_track_point_in_utc_time(
self.hass, self.point_in_time_listener, self.get_next_interval(now)

View File

@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "1"
PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)

View File

@ -5,8 +5,10 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from functools import cache, partial
from typing import Any
import slugify as unicode_slug
import voluptuous as vol
from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE
@ -175,10 +177,11 @@ class IntentTool(Tool):
def __init__(
self,
name: str,
intent_handler: intent.IntentHandler,
) -> None:
"""Init the class."""
self.name = intent_handler.intent_type
self.name = name
self.description = (
intent_handler.description or f"Execute Home Assistant {self.name} intent"
)
@ -261,6 +264,9 @@ class AssistAPI(API):
id=LLM_API_ASSIST,
name="Assist",
)
self.cached_slugify = cache(
partial(unicode_slug.slugify, separator="_", lowercase=False)
)
async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance:
"""Return the instance of the API."""
@ -373,7 +379,10 @@ class AssistAPI(API):
or intent_handler.platforms & exposed_domains
]
return [IntentTool(intent_handler) for intent_handler in intent_handlers]
return [
IntentTool(self.cached_slugify(intent_handler.intent_type), intent_handler)
for intent_handler in intent_handlers
]
def _get_exposed_entities(

View File

@ -1758,10 +1758,6 @@ class Script:
# runs before sleeping as otherwise if two runs are started at the exact
# same time they will cancel each other out.
self._log("Restarting")
# Important: yield to the event loop to allow the script to start in case
# the script is restarting itself so it ends up in the script stack and
# the recursion check above will prevent the script from running.
await asyncio.sleep(0)
await self.async_stop(update_state=False, spare=run)
if started_action:

View File

@ -32,7 +32,7 @@ habluetooth==3.1.1
hass-nabucasa==0.81.1
hassil==1.7.1
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240605.0
home-assistant-frontend==20240610.0
home-assistant-intents==2024.6.5
httpx==0.27.0
ifaddr==0.2.0

View File

@ -1393,16 +1393,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.electrasmart.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.electric_kiwi.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2024.6.1"
version = "2024.6.2"
license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3."
readme = "README.rst"

View File

@ -353,7 +353,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==10.0.0
aioshelly==10.0.1
# homeassistant.components.skybell
aioskybell==22.7.0
@ -389,7 +389,7 @@ aiovlc==0.3.2
aiovodafone==0.6.0
# homeassistant.components.waqi
aiowaqi==3.0.1
aiowaqi==3.1.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -912,7 +912,7 @@ fyta_cli==0.4.1
gTTS==2.2.4
# homeassistant.components.gardena_bluetooth
gardena-bluetooth==1.4.1
gardena-bluetooth==1.4.2
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.11
@ -955,7 +955,7 @@ gios==4.0.0
gitterpy==0.1.7
# homeassistant.components.glances
glances-api==0.7.0
glances-api==0.8.0
# homeassistant.components.goalzero
goalzero==0.2.2
@ -974,10 +974,10 @@ google-cloud-pubsub==2.13.11
google-cloud-texttospeech==2.12.3
# homeassistant.components.google_generative_ai_conversation
google-generativeai==0.5.4
google-generativeai==0.6.0
# homeassistant.components.nest
google-nest-sdm==4.0.4
google-nest-sdm==4.0.5
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@ -1087,7 +1087,7 @@ hole==0.8.0
holidays==0.50
# homeassistant.components.frontend
home-assistant-frontend==20240605.0
home-assistant-frontend==20240610.0
# homeassistant.components.conversation
home-assistant-intents==2024.6.5
@ -1146,7 +1146,7 @@ iglo==1.2.7
ihcsdk==2.8.5
# homeassistant.components.imgw_pib
imgw_pib==1.0.4
imgw_pib==1.0.5
# homeassistant.components.incomfort
incomfort-client==0.5.0
@ -1501,7 +1501,7 @@ openwrt-luci-rpc==1.1.17
openwrt-ubus-rpc==0.0.2
# homeassistant.components.opower
opower==0.4.6
opower==0.4.7
# homeassistant.components.oralb
oralb-ble==0.17.6
@ -1649,7 +1649,7 @@ py-schluter==0.1.7
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.4.2
py-synologydsm-api==2.4.4
# homeassistant.components.zabbix
py-zabbix==1.1.7
@ -1670,7 +1670,7 @@ pyControl4==1.1.0
pyDuotecno==2024.5.1
# homeassistant.components.electrasmart
pyElectra==1.2.0
pyElectra==1.2.1
# homeassistant.components.emby
pyEmby==1.9
@ -2306,7 +2306,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3
# homeassistant.components.roborock
python-roborock==2.2.3
python-roborock==2.3.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@ -2357,9 +2357,6 @@ pytrydan==0.6.1
# homeassistant.components.usb
pyudev==0.24.1
# homeassistant.components.unifiprotect
pyunifiprotect==5.1.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -2764,7 +2761,7 @@ transmission-rpc==7.0.3
ttls==1.5.1
# homeassistant.components.thethingsnetwork
ttn_client==0.0.4
ttn_client==1.0.0
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9
@ -2781,6 +2778,9 @@ twitchAPI==4.0.0
# homeassistant.components.ukraine_alarm
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==0.4.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@ -326,7 +326,7 @@ aioruuvigateway==0.1.0
aiosenz==1.0.0
# homeassistant.components.shelly
aioshelly==10.0.0
aioshelly==10.0.1
# homeassistant.components.skybell
aioskybell==22.7.0
@ -362,7 +362,7 @@ aiovlc==0.3.2
aiovodafone==0.6.0
# homeassistant.components.waqi
aiowaqi==3.0.1
aiowaqi==3.1.0
# homeassistant.components.watttime
aiowatttime==0.1.1
@ -747,7 +747,7 @@ fyta_cli==0.4.1
gTTS==2.2.4
# homeassistant.components.gardena_bluetooth
gardena-bluetooth==1.4.1
gardena-bluetooth==1.4.2
# homeassistant.components.google_assistant_sdk
gassist-text==0.0.11
@ -784,7 +784,7 @@ getmac==0.9.4
gios==4.0.0
# homeassistant.components.glances
glances-api==0.7.0
glances-api==0.8.0
# homeassistant.components.goalzero
goalzero==0.2.2
@ -800,10 +800,10 @@ google-api-python-client==2.71.0
google-cloud-pubsub==2.13.11
# homeassistant.components.google_generative_ai_conversation
google-generativeai==0.5.4
google-generativeai==0.6.0
# homeassistant.components.nest
google-nest-sdm==4.0.4
google-nest-sdm==4.0.5
# homeassistant.components.google_travel_time
googlemaps==2.5.1
@ -889,7 +889,7 @@ hole==0.8.0
holidays==0.50
# homeassistant.components.frontend
home-assistant-frontend==20240605.0
home-assistant-frontend==20240610.0
# homeassistant.components.conversation
home-assistant-intents==2024.6.5
@ -933,7 +933,7 @@ idasen-ha==2.5.3
ifaddr==0.2.0
# homeassistant.components.imgw_pib
imgw_pib==1.0.4
imgw_pib==1.0.5
# homeassistant.components.influxdb
influxdb-client==1.24.0
@ -1201,7 +1201,7 @@ openhomedevice==2.2.0
openwebifpy==4.2.4
# homeassistant.components.opower
opower==0.4.6
opower==0.4.7
# homeassistant.components.oralb
oralb-ble==0.17.6
@ -1311,7 +1311,7 @@ py-nightscout==1.2.2
py-sucks==0.9.10
# homeassistant.components.synology_dsm
py-synologydsm-api==2.4.2
py-synologydsm-api==2.4.4
# homeassistant.components.seventeentrack
py17track==2021.12.2
@ -1326,7 +1326,7 @@ pyControl4==1.1.0
pyDuotecno==2024.5.1
# homeassistant.components.electrasmart
pyElectra==1.2.0
pyElectra==1.2.1
# homeassistant.components.rfxtrx
pyRFXtrx==0.31.1
@ -1794,7 +1794,7 @@ python-qbittorrent==0.4.3
python-rabbitair==0.0.8
# homeassistant.components.roborock
python-roborock==2.2.3
python-roborock==2.3.0
# homeassistant.components.smarttub
python-smarttub==0.0.36
@ -1836,9 +1836,6 @@ pytrydan==0.6.1
# homeassistant.components.usb
pyudev==0.24.1
# homeassistant.components.unifiprotect
pyunifiprotect==5.1.2
# homeassistant.components.uptimerobot
pyuptimerobot==22.2.0
@ -2138,7 +2135,7 @@ transmission-rpc==7.0.3
ttls==1.5.1
# homeassistant.components.thethingsnetwork
ttn_client==0.0.4
ttn_client==1.0.0
# homeassistant.components.tuya
tuya-device-sharing-sdk==0.1.9
@ -2155,6 +2152,9 @@ twitchAPI==4.0.0
# homeassistant.components.ukraine_alarm
uasiren==0.0.1
# homeassistant.components.unifiprotect
uiprotect==0.4.1
# homeassistant.components.landisgyr_heat_meter
ultraheat-api==0.5.7

View File

@ -30,7 +30,6 @@ PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$")
IGNORE_STANDARD_LIBRARY_VIOLATIONS = {
# Integrations which have standard library requirements.
"electrasmart",
"slide",
"suez_water",
}

View File

@ -2771,6 +2771,7 @@ async def test_recursive_automation_starting_script(
],
"action": [
{"service": "test.automation_started"},
{"delay": 0.001},
{"service": "script.script1"},
],
}
@ -2817,7 +2818,10 @@ async def test_recursive_automation_starting_script(
assert script_warning_msg in caplog.text
@pytest.mark.parametrize("automation_mode", SCRIPT_MODE_CHOICES)
@pytest.mark.parametrize(
"automation_mode",
[mode for mode in SCRIPT_MODE_CHOICES if mode != SCRIPT_MODE_RESTART],
)
@pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True])
async def test_recursive_automation(
hass: HomeAssistant, automation_mode, caplog: pytest.LogCaptureFixture
@ -2878,6 +2882,68 @@ async def test_recursive_automation(
assert "Disallowed recursion detected" not in caplog.text
@pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True])
async def test_recursive_automation_restart_mode(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test automation restarting itself.
The automation is an infinite loop since it keeps restarting itself
- Illegal recursion detection should not be triggered
- Home Assistant should not hang on shut down
"""
stop_scripts_at_shutdown_called = asyncio.Event()
real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown
async def stop_scripts_at_shutdown(*args):
await real_stop_scripts_at_shutdown(*args)
stop_scripts_at_shutdown_called.set()
with patch(
"homeassistant.helpers.script._async_stop_scripts_at_shutdown",
wraps=stop_scripts_at_shutdown,
):
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"mode": SCRIPT_MODE_RESTART,
"trigger": [
{"platform": "event", "event_type": "trigger_automation"},
],
"action": [
{"event": "trigger_automation"},
{"service": "test.automation_done"},
],
}
},
)
service_called = asyncio.Event()
async def async_service_handler(service):
if service.service == "automation_done":
service_called.set()
hass.services.async_register("test", "automation_done", async_service_handler)
hass.bus.async_fire("trigger_automation")
await asyncio.sleep(0)
# Trigger 1st stage script shutdown
hass.set_state(CoreState.stopping)
hass.bus.async_fire("homeassistant_stop")
await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1)
# Trigger 2nd stage script shutdown
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90))
await hass.async_block_till_done()
assert "Disallowed recursion detected" not in caplog.text
async def test_websocket_config(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
@ -3097,3 +3163,72 @@ async def test_two_automations_call_restart_script_same_time(
await hass.async_block_till_done()
assert len(events) == 2
cancel()
async def test_two_automation_call_restart_script_right_after_each_other(
hass: HomeAssistant,
) -> None:
"""Test two automations call a restart script right after each other."""
events = async_capture_events(hass, "repeat_test_script_finished")
assert await async_setup_component(
hass,
input_boolean.DOMAIN,
{
input_boolean.DOMAIN: {
"test_1": None,
"test_2": None,
}
},
)
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": "state",
"entity_id": ["input_boolean.test_1", "input_boolean.test_1"],
"from": "off",
"to": "on",
},
"action": [
{
"repeat": {
"count": 2,
"sequence": [
{
"delay": {
"hours": 0,
"minutes": 0,
"seconds": 0,
"milliseconds": 100,
}
}
],
}
},
{"event": "repeat_test_script_finished", "event_data": {}},
],
"id": "automation_0",
"mode": "restart",
},
]
},
)
hass.states.async_set("input_boolean.test_1", "off")
hass.states.async_set("input_boolean.test_2", "off")
await hass.async_block_till_done()
hass.states.async_set("input_boolean.test_1", "on")
hass.states.async_set("input_boolean.test_2", "on")
await asyncio.sleep(0)
hass.states.async_set("input_boolean.test_1", "off")
hass.states.async_set("input_boolean.test_2", "off")
await asyncio.sleep(0)
hass.states.async_set("input_boolean.test_1", "on")
hass.states.async_set("input_boolean.test_2", "on")
await hass.async_block_till_done()
assert len(events) == 1

View File

@ -8,7 +8,7 @@ from homeassistant.components.azure_data_explorer.const import (
CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID,
CONF_SEND_INTERVAL,
CONF_USE_FREE,
CONF_USE_QUEUED_CLIENT,
)
AZURE_DATA_EXPLORER_PATH = "homeassistant.components.azure_data_explorer"
@ -29,7 +29,7 @@ BASE_CONFIG_URI = {
}
BASIC_OPTIONS = {
CONF_USE_FREE: False,
CONF_USE_QUEUED_CLIENT: False,
CONF_SEND_INTERVAL: 5,
}
@ -39,10 +39,10 @@ BASE_CONFIG_FULL = BASE_CONFIG | BASIC_OPTIONS | BASE_CONFIG_URI
BASE_CONFIG_IMPORT = {
CONF_ADX_CLUSTER_INGEST_URI: "https://cluster.region.kusto.windows.net",
CONF_USE_FREE: False,
CONF_USE_QUEUED_CLIENT: False,
CONF_SEND_INTERVAL: 5,
}
FREE_OPTIONS = {CONF_USE_FREE: True, CONF_SEND_INTERVAL: 5}
FREE_OPTIONS = {CONF_USE_QUEUED_CLIENT: True, CONF_SEND_INTERVAL: 5}
BASE_CONFIG_FREE = BASE_CONFIG | FREE_OPTIONS

View File

@ -1,5 +1,5 @@
{
"productName": "Elgato Key Light",
"productName": "Elgato Light Strip",
"hardwareBoardType": 53,
"firmwareBuildNumber": 192,
"firmwareVersion": "1.0.3",

View File

@ -218,7 +218,7 @@
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'model': 'Elgato Light Strip',
'name': 'Frenck',
'name_by_user': None,
'serial_number': 'CN11A1A00001',
@ -333,7 +333,7 @@
'labels': set({
}),
'manufacturer': 'Elgato',
'model': 'Elgato Key Light',
'model': 'Elgato Light Strip',
'name': 'Frenck',
'name_by_user': None,
'serial_number': 'CN11A1A00001',

View File

@ -1931,7 +1931,10 @@ async def test_arm_disarm_disarm(hass: HomeAssistant) -> None:
}
}
assert trt.query_attributes() == {"isArmed": False}
assert trt.query_attributes() == {
"currentArmLevel": "armed_custom_bypass",
"isArmed": False,
}
assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False})

View File

@ -12,6 +12,9 @@ import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.components.conversation import trace
from homeassistant.components.google_generative_ai_conversation.conversation import (
_escape_decode,
)
from homeassistant.const import CONF_LLM_HASS_API
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -504,3 +507,18 @@ async def test_conversation_agent(
mock_config_entry.entry_id
)
assert agent.supported_languages == "*"
async def test_escape_decode() -> None:
"""Test _escape_decode."""
assert _escape_decode(
{
"param1": ["test_value", "param1\\'s value"],
"param2": "param2\\'s value",
"param3": {"param31": "Cheminée", "param32": "Chemin\\303\\251e"},
}
) == {
"param1": ["test_value", "param1's value"],
"param2": "param2's value",
"param3": {"param31": "Cheminée", "param32": "Cheminée"},
}

View File

@ -763,3 +763,52 @@ async def test_last_sensor(hass: HomeAssistant) -> None:
state = hass.states.get("sensor.test_last")
assert str(float(value)) == state.state
assert entity_id == state.attributes.get("last_entity_id")
async def test_sensors_attributes_added_when_entity_info_available(
hass: HomeAssistant,
) -> None:
"""Test the sensor calculate attributes once all entities attributes are available."""
config = {
SENSOR_DOMAIN: {
"platform": GROUP_DOMAIN,
"name": DEFAULT_NAME,
"type": "sum",
"entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"],
"unique_id": "very_unique_id",
}
}
entity_ids = config["sensor"]["entities"]
assert await async_setup_component(hass, "sensor", config)
await hass.async_block_till_done()
state = hass.states.get("sensor.sensor_group_sum")
assert state.state == STATE_UNAVAILABLE
assert state.attributes.get(ATTR_ENTITY_ID) is None
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
assert state.attributes.get(ATTR_STATE_CLASS) is None
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(
entity_id,
value,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME,
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
ATTR_UNIT_OF_MEASUREMENT: "L",
},
)
await hass.async_block_till_done()
state = hass.states.get("sensor.sensor_group_sum")
assert float(state.state) == pytest.approx(float(SUM_VALUE))
assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME
assert state.attributes.get(ATTR_ICON) is None
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L"

View File

@ -305,4 +305,4 @@ async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
}
assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address
assert len(mock_setup_entry.mock_calls) == 1
desk_connect.assert_called_with(ANY, auto_reconnect=False)
desk_connect.assert_called_with(ANY, retry=False)

View File

@ -7,6 +7,7 @@ import threading
from unittest.mock import ANY, patch
from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components import recorder
@ -794,6 +795,347 @@ async def test_statistic_during_period_hole(
}
@pytest.mark.parametrize(
"frozen_time",
[
# This is the normal case, all statistics runs are available
datetime.datetime(2022, 10, 21, 6, 31, tzinfo=datetime.UTC),
# Statistic only available up until 6:25, this can happen if
# core has been shut down for an hour
datetime.datetime(2022, 10, 21, 7, 31, tzinfo=datetime.UTC),
],
)
async def test_statistic_during_period_partial_overlap(
recorder_mock: Recorder,
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
freezer: FrozenDateTimeFactory,
frozen_time: datetime,
) -> None:
"""Test statistic_during_period."""
client = await hass_ws_client()
freezer.move_to(frozen_time)
now = dt_util.utcnow()
await async_recorder_block_till_done(hass)
zero = now
start = zero.replace(hour=0, minute=0, second=0, microsecond=0)
# Sum shall be tracking a hypothetical sensor that is 0 at midnight, and grows by 1 per minute.
# The test will have 4 hours of LTS-only data (0:00-3:59:59), followed by 2 hours of overlapping STS/LTS (4:00-5:59:59), followed by 30 minutes of STS only (6:00-6:29:59)
# similar to how a real recorder might look after purging STS.
# The datapoint at i=0 (start = 0:00) will be 60 as that is the growth during the hour starting at the start period
imported_stats_hours = [
{
"start": (start + timedelta(hours=i)),
"min": i * 60,
"max": i * 60 + 60,
"mean": i * 60 + 30,
"sum": (i + 1) * 60,
}
for i in range(6)
]
# The datapoint at i=0 (start = 4:00) would be the sensor's value at t=4:05, or 245
imported_stats_5min = [
{
"start": (start + timedelta(hours=4, minutes=5 * i)),
"min": 4 * 60 + i * 5,
"max": 4 * 60 + i * 5 + 5,
"mean": 4 * 60 + i * 5 + 2.5,
"sum": 4 * 60 + (i + 1) * 5,
}
for i in range(30)
]
assert imported_stats_hours[-1]["sum"] == 360
assert imported_stats_hours[-1]["start"] == start.replace(
hour=5, minute=0, second=0, microsecond=0
)
assert imported_stats_5min[-1]["sum"] == 390
assert imported_stats_5min[-1]["start"] == start.replace(
hour=6, minute=25, second=0, microsecond=0
)
statId = "sensor.test_overlapping"
imported_metadata = {
"has_mean": False,
"has_sum": True,
"name": "Total imported energy overlapping",
"source": "recorder",
"statistic_id": statId,
"unit_of_measurement": "kWh",
}
recorder.get_instance(hass).async_import_statistics(
imported_metadata,
imported_stats_hours,
Statistics,
)
recorder.get_instance(hass).async_import_statistics(
imported_metadata,
imported_stats_5min,
StatisticsShortTerm,
)
await async_wait_recording_done(hass)
metadata = get_metadata(hass, statistic_ids={statId})
metadata_id = metadata[statId][0]
run_cache = get_short_term_statistics_run_cache(hass)
# Verify the import of the short term statistics
# also updates the run cache
assert run_cache.get_latest_ids({metadata_id}) is not None
# Get all the stats, should consider all hours and 5mins
await client.send_json_auto_id(
{
"type": "recorder/statistic_during_period",
"statistic_id": statId,
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {
"change": 390,
"max": 390,
"min": 0,
"mean": 195,
}
async def assert_stat_during_fixed(client, start_time, end_time, expect):
json = {
"type": "recorder/statistic_during_period",
"types": list(expect.keys()),
"statistic_id": statId,
"fixed_period": {},
}
if start_time:
json["fixed_period"]["start_time"] = start_time.isoformat()
if end_time:
json["fixed_period"]["end_time"] = end_time.isoformat()
await client.send_json_auto_id(json)
response = await client.receive_json()
assert response["success"]
assert response["result"] == expect
# One hours worth of growth in LTS-only
start_time = start.replace(hour=1)
end_time = start.replace(hour=2)
await assert_stat_during_fixed(
client, start_time, end_time, {"change": 60, "min": 60, "max": 120, "mean": 90}
)
# Five minutes of growth in STS-only
start_time = start.replace(hour=6, minute=15)
end_time = start.replace(hour=6, minute=20)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 5,
"min": 6 * 60 + 15,
"max": 6 * 60 + 20,
"mean": 6 * 60 + (15 + 20) / 2,
},
)
# Six minutes of growth in STS-only
start_time = start.replace(hour=6, minute=14)
end_time = start.replace(hour=6, minute=20)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 5,
"min": 6 * 60 + 15,
"max": 6 * 60 + 20,
"mean": 6 * 60 + (15 + 20) / 2,
},
)
# Six minutes of growth in STS-only
# 5-minute Change includes start times exactly on or before a statistics start, but end times are not counted unless they are greater than start.
start_time = start.replace(hour=6, minute=15)
end_time = start.replace(hour=6, minute=21)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 10,
"min": 6 * 60 + 15,
"max": 6 * 60 + 25,
"mean": 6 * 60 + (15 + 25) / 2,
},
)
# Five minutes of growth in overlapping LTS+STS
start_time = start.replace(hour=5, minute=15)
end_time = start.replace(hour=5, minute=20)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 5,
"min": 5 * 60 + 15,
"max": 5 * 60 + 20,
"mean": 5 * 60 + (15 + 20) / 2,
},
)
# Five minutes of growth in overlapping LTS+STS (start of hour)
start_time = start.replace(hour=5, minute=0)
end_time = start.replace(hour=5, minute=5)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 5, "min": 5 * 60, "max": 5 * 60 + 5, "mean": 5 * 60 + (5) / 2},
)
# Five minutes of growth in overlapping LTS+STS (end of hour)
start_time = start.replace(hour=4, minute=55)
end_time = start.replace(hour=5, minute=0)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 5,
"min": 4 * 60 + 55,
"max": 5 * 60,
"mean": 4 * 60 + (55 + 60) / 2,
},
)
# Five minutes of growth in STS-only, with a minute offset. Despite that this does not cover the full period, result is still 5
start_time = start.replace(hour=6, minute=16)
end_time = start.replace(hour=6, minute=21)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 5,
"min": 6 * 60 + 20,
"max": 6 * 60 + 25,
"mean": 6 * 60 + (20 + 25) / 2,
},
)
# 7 minutes of growth in STS-only, spanning two intervals
start_time = start.replace(hour=6, minute=14)
end_time = start.replace(hour=6, minute=21)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 10,
"min": 6 * 60 + 15,
"max": 6 * 60 + 25,
"mean": 6 * 60 + (15 + 25) / 2,
},
)
# One hours worth of growth in LTS-only, with arbitrary minute offsets
# Since this does not fully cover the hour, result is None?
start_time = start.replace(hour=1, minute=40)
end_time = start.replace(hour=2, minute=12)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": None, "min": None, "max": None, "mean": None},
)
# One hours worth of growth in LTS-only, with arbitrary minute offsets, covering a whole 1-hour period
start_time = start.replace(hour=1, minute=40)
end_time = start.replace(hour=3, minute=12)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 60, "min": 120, "max": 180, "mean": 150},
)
# 90 minutes of growth in window overlapping LTS+STS/STS-only (4:41 - 6:11)
start_time = start.replace(hour=4, minute=41)
end_time = start_time + timedelta(minutes=90)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{
"change": 90,
"min": 4 * 60 + 45,
"max": 4 * 60 + 45 + 90,
"mean": 4 * 60 + 45 + 45,
},
)
# 4 hours of growth in overlapping LTS-only/LTS+STS (2:01-6:01)
start_time = start.replace(hour=2, minute=1)
end_time = start_time + timedelta(minutes=240)
# 60 from LTS (3:00-3:59), 125 from STS (25 intervals) (4:00-6:01)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 185, "min": 3 * 60, "max": 3 * 60 + 185, "mean": 3 * 60 + 185 / 2},
)
# 4 hours of growth in overlapping LTS-only/LTS+STS (1:31-5:31)
start_time = start.replace(hour=1, minute=31)
end_time = start_time + timedelta(minutes=240)
# 120 from LTS (2:00-3:59), 95 from STS (19 intervals) 4:00-5:31
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 215, "min": 2 * 60, "max": 2 * 60 + 215, "mean": 2 * 60 + 215 / 2},
)
# 5 hours of growth, start time only (1:31-end)
start_time = start.replace(hour=1, minute=31)
end_time = None
# will be actually 2:00 - end
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 4 * 60 + 30, "min": 120, "max": 390, "mean": (390 + 120) / 2},
)
# 5 hours of growth, end_time_only (0:00-5:00)
start_time = None
end_time = start.replace(hour=5)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 5 * 60, "min": 0, "max": 5 * 60, "mean": (5 * 60) / 2},
)
# 5 hours 1 minute of growth, end_time_only (0:00-5:01)
start_time = None
end_time = start.replace(hour=5, minute=1)
# 4 hours LTS, 1 hour and 5 minutes STS (4:00-5:01)
await assert_stat_during_fixed(
client,
start_time,
end_time,
{"change": 5 * 60 + 5, "min": 0, "max": 5 * 60 + 5, "mean": (5 * 60 + 5) / 2},
)
@pytest.mark.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC))
@pytest.mark.parametrize(
("calendar_period", "start_time", "end_time"),

View File

@ -220,10 +220,14 @@ async def test_incorrectly_formatted_mac_fixed(hass: HomeAssistant) -> None:
@pytest.mark.usefixtures("remotews", "rest_api")
@pytest.mark.xfail
async def test_cleanup_mac(
hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion
) -> None:
"""Test for `none` mac cleanup #103512."""
"""Test for `none` mac cleanup #103512.
Reverted due to device registry collisions in #119249 / #119082
"""
entry = MockConfigEntry(
domain=SAMSUNGTV_DOMAIN,
data=MOCK_ENTRY_WS_WITH_MAC,

View File

@ -15,12 +15,14 @@ from homeassistant.components.shelly.const import (
ATTR_CLICK_TYPE,
ATTR_DEVICE,
ATTR_GENERATION,
CONF_BLE_SCANNER_MODE,
DOMAIN,
ENTRY_RELOAD_COOLDOWN,
MAX_PUSH_UPDATE_FAILURES,
RPC_RECONNECT_INTERVAL,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
BLEScannerMode,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE
@ -485,6 +487,25 @@ async def test_rpc_reload_with_invalid_auth(
assert flow["context"].get("entry_id") == entry.entry_id
async def test_rpc_connection_error_during_unload(
hass: HomeAssistant, mock_rpc_device: Mock, caplog: pytest.LogCaptureFixture
) -> None:
"""Test RPC DeviceConnectionError suppressed during config entry unload."""
entry = await init_integration(hass, 2)
assert entry.state is ConfigEntryState.LOADED
with patch(
"homeassistant.components.shelly.coordinator.async_stop_scanner",
side_effect=DeviceConnectionError,
):
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert "Error during shutdown for device" in caplog.text
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_rpc_click_event(
hass: HomeAssistant,
mock_rpc_device: Mock,
@ -713,6 +734,32 @@ async def test_rpc_reconnect_error(
assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE
async def test_rpc_error_running_connected_events(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_rpc_device: Mock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test RPC error while running connected events."""
with patch(
"homeassistant.components.shelly.coordinator.async_ensure_ble_enabled",
side_effect=DeviceConnectionError,
):
await init_integration(
hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE}
)
assert "Error running connected events for device" in caplog.text
assert get_entity_state(hass, "switch.test_switch_0") == STATE_UNAVAILABLE
# Move time to generate reconnect without error
freezer.tick(timedelta(seconds=RPC_RECONNECT_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert get_entity_state(hass, "switch.test_switch_0") == STATE_ON
async def test_rpc_polling_connection_error(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,

View File

@ -13,8 +13,8 @@ from typing import Any
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pyunifiprotect import ProtectApiClient
from pyunifiprotect.data import (
from uiprotect import ProtectApiClient
from uiprotect.data import (
NVR,
Bootstrap,
Camera,

View File

@ -5,8 +5,8 @@ from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import Mock
from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor
from pyunifiprotect.data.nvr import EventMetadata
from uiprotect.data import Camera, Event, EventType, Light, MountType, Sensor
from uiprotect.data.nvr import EventMetadata
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.unifiprotect.binary_sensor import (

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data.devices import Camera, Chime, Doorlock
from uiprotect.data.devices import Camera, Chime, Doorlock
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType
from pyunifiprotect.exceptions import NvrError
from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType
from uiprotect.exceptions import NvrError
from homeassistant.components.camera import (
CameraEntityFeature,

View File

@ -7,8 +7,8 @@ import socket
from unittest.mock import patch
import pytest
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, CloudAccount
from uiprotect import NotAuthorized, NvrError, ProtectApiClient
from uiprotect.data import NVR, Bootstrap, CloudAccount
from homeassistant import config_entries
from homeassistant.components import dhcp, ssdp

View File

@ -1,6 +1,6 @@
"""Test UniFi Protect diagnostics."""
from pyunifiprotect.data import NVR, Light
from uiprotect.data import NVR, Light
from homeassistant.components.unifiprotect.const import CONF_ALLOW_EA
from homeassistant.core import HomeAssistant

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock, patch
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import NVR, Bootstrap, CloudAccount, Light
from uiprotect import NotAuthorized, NvrError, ProtectApiClient
from uiprotect.data import NVR, Bootstrap, CloudAccount, Light
from homeassistant.components.unifiprotect.const import (
AUTH_RETRIES,

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Light
from pyunifiprotect.data.types import LEDLevel
from uiprotect.data import Light
from uiprotect.data.types import LEDLevel
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Doorlock, LockStatusType
from uiprotect.data import Doorlock, LockStatusType
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.const import (

View File

@ -5,8 +5,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pyunifiprotect.data import Camera
from pyunifiprotect.exceptions import StreamError
from uiprotect.data import Camera
from uiprotect.exceptions import StreamError
from homeassistant.components.media_player import (
ATTR_MEDIA_CONTENT_TYPE,

View File

@ -5,7 +5,7 @@ from ipaddress import IPv4Address
from unittest.mock import AsyncMock, Mock, patch
import pytest
from pyunifiprotect.data import (
from uiprotect.data import (
Bootstrap,
Camera,
Event,
@ -13,7 +13,7 @@ from pyunifiprotect.data import (
Permission,
SmartDetectObjectType,
)
from pyunifiprotect.exceptions import NvrError
from uiprotect.exceptions import NvrError
from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source import MediaSourceItem

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import patch
from pyunifiprotect.data import Camera
from uiprotect.data import Camera
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.repairs.issue_handler import (

View File

@ -6,7 +6,7 @@ from datetime import timedelta
from unittest.mock import AsyncMock, Mock
import pytest
from pyunifiprotect.data import Camera, Doorlock, IRLEDMode, Light
from uiprotect.data import Camera, Doorlock, IRLEDMode, Light
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.components.unifiprotect.number import (

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
from unittest.mock import Mock
from pyunifiprotect.data import Camera, Event, EventType
from uiprotect.data import Camera, Event, EventType
from homeassistant.components.recorder import Recorder
from homeassistant.components.recorder.history import get_significant_states

View File

@ -6,7 +6,7 @@ from copy import copy, deepcopy
from http import HTTPStatus
from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Camera, CloudAccount, ModelType, Version
from uiprotect.data import Camera, CloudAccount, ModelType, Version
from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms,

Some files were not shown because too many files have changed in this diff Show More