mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
2024.6.2 (#119376)
This commit is contained in:
commit
090d296135
@ -163,7 +163,6 @@ homeassistant.components.easyenergy.*
|
||||
homeassistant.components.ecovacs.*
|
||||
homeassistant.components.ecowitt.*
|
||||
homeassistant.components.efergy.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
|
@ -1486,8 +1486,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/unifi/ @Kane610
|
||||
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
|
||||
/homeassistant/components/unifiled/ @florisvdk
|
||||
/homeassistant/components/unifiprotect/ @bdraco
|
||||
/tests/components/unifiprotect/ @bdraco
|
||||
/homeassistant/components/upb/ @gwww
|
||||
/tests/components/upb/ @gwww
|
||||
/homeassistant/components/upc_connect/ @pvizeli @fabaff
|
||||
|
@ -2,5 +2,5 @@
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
|
||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
||||
|
@ -62,13 +62,12 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool:
|
||||
|
||||
Adds an empty filter to hass data.
|
||||
Tries to get a filter from yaml, if present set to hass data.
|
||||
If config is empty after getting the filter, return, otherwise emit
|
||||
deprecated warning and pass the rest to the config flow.
|
||||
"""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: {}})
|
||||
hass.data.setdefault(DOMAIN, {DATA_FILTER: FILTER_SCHEMA({})})
|
||||
if DOMAIN in yaml_config:
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN][CONF_FILTER]
|
||||
hass.data[DOMAIN][DATA_FILTER] = yaml_config[DOMAIN].pop(CONF_FILTER)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -207,6 +206,6 @@ class AzureDataExplorer:
|
||||
if "\n" in state.state:
|
||||
return None, dropped + 1
|
||||
|
||||
json_event = str(json.dumps(obj=state, cls=JSONEncoder).encode("utf-8"))
|
||||
json_event = json.dumps(obj=state, cls=JSONEncoder)
|
||||
|
||||
return (json_event, dropped)
|
||||
|
@ -23,7 +23,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -35,7 +35,6 @@ class AzureDataExplorerClient:
|
||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
||||
"""Create the right class."""
|
||||
|
||||
self._cluster_ingest_uri = data[CONF_ADX_CLUSTER_INGEST_URI]
|
||||
self._database = data[CONF_ADX_DATABASE_NAME]
|
||||
self._table = data[CONF_ADX_TABLE_NAME]
|
||||
self._ingestion_properties = IngestionProperties(
|
||||
@ -45,24 +44,36 @@ class AzureDataExplorerClient:
|
||||
ingestion_mapping_reference="ha_json_mapping",
|
||||
)
|
||||
|
||||
# Create cLient for ingesting and querying data
|
||||
kcsb = KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
self._cluster_ingest_uri,
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
# Create client for ingesting data
|
||||
kcsb_ingest = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI],
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
if data[CONF_USE_FREE] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb)
|
||||
# Create client for querying data
|
||||
kcsb_query = (
|
||||
KustoConnectionStringBuilder.with_aad_application_key_authentication(
|
||||
data[CONF_ADX_CLUSTER_INGEST_URI].replace("ingest-", ""),
|
||||
data[CONF_APP_REG_ID],
|
||||
data[CONF_APP_REG_SECRET],
|
||||
data[CONF_AUTHORITY_ID],
|
||||
)
|
||||
)
|
||||
|
||||
self.query_client = KustoClient(kcsb)
|
||||
if data[CONF_USE_QUEUED_CLIENT] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb_ingest)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
|
||||
|
||||
self.query_client = KustoClient(kcsb_query)
|
||||
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception when it cannot connect."""
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
query = f"{self._table} | take 1"
|
||||
|
||||
|
@ -10,6 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers.selector import BooleanSelector
|
||||
|
||||
from . import AzureDataExplorerClient
|
||||
from .const import (
|
||||
@ -19,7 +20,7 @@ from .const import (
|
||||
CONF_APP_REG_ID,
|
||||
CONF_APP_REG_SECRET,
|
||||
CONF_AUTHORITY_ID,
|
||||
CONF_USE_FREE,
|
||||
CONF_USE_QUEUED_CLIENT,
|
||||
DEFAULT_OPTIONS,
|
||||
DOMAIN,
|
||||
)
|
||||
@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_APP_REG_ID): str,
|
||||
vol.Required(CONF_APP_REG_SECRET): str,
|
||||
vol.Required(CONF_AUTHORITY_ID): str,
|
||||
vol.Optional(CONF_USE_FREE, default=False): bool,
|
||||
vol.Required(CONF_USE_QUEUED_CLIENT, default=False): BooleanSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -17,7 +17,7 @@ CONF_AUTHORITY_ID = "authority_id"
|
||||
CONF_SEND_INTERVAL = "send_interval"
|
||||
CONF_MAX_DELAY = "max_delay"
|
||||
CONF_FILTER = DATA_FILTER = "filter"
|
||||
CONF_USE_FREE = "use_queued_ingestion"
|
||||
CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
|
||||
DATA_HUB = "hub"
|
||||
STEP_USER = "user"
|
||||
|
||||
|
@ -3,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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -120,7 +120,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
director_all_items = json.loads(director_all_items)
|
||||
entry_data[CONF_DIRECTOR_ALL_ITEMS] = director_all_items
|
||||
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(await director.getUiConfiguration())
|
||||
# Check if OS version is 3 or higher to get UI configuration
|
||||
entry_data[CONF_UI_CONFIGURATION] = None
|
||||
if int(entry_data[CONF_DIRECTOR_SW_VERSION].split(".")[0]) >= 3:
|
||||
entry_data[CONF_UI_CONFIGURATION] = json.loads(
|
||||
await director.getUiConfiguration()
|
||||
)
|
||||
|
||||
# Load options from config entry
|
||||
entry_data[CONF_SCAN_INTERVAL] = entry.options.get(
|
||||
|
@ -81,11 +81,18 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up Control4 rooms from a config entry."""
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
# OS 2 will not have a ui_configuration
|
||||
if not ui_config:
|
||||
_LOGGER.debug("No UI Configuration found for Control4")
|
||||
return
|
||||
|
||||
all_rooms = await get_rooms(hass, entry)
|
||||
if not all_rooms:
|
||||
return
|
||||
|
||||
entry_data = hass.data[DOMAIN][entry.entry_id]
|
||||
scan_interval = entry_data[CONF_SCAN_INTERVAL]
|
||||
_LOGGER.debug("Scan interval = %s", scan_interval)
|
||||
|
||||
@ -119,8 +126,6 @@ async def async_setup_entry(
|
||||
if "parentId" in item and k > 1
|
||||
}
|
||||
|
||||
ui_config = entry_data[CONF_UI_CONFIGURATION]
|
||||
|
||||
entity_list = []
|
||||
for room in all_rooms:
|
||||
room_id = room["id"]
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyElectra==1.2.0"]
|
||||
"requirements": ["pyElectra==1.2.1"]
|
||||
}
|
||||
|
@ -59,7 +59,15 @@ class ElgatoLight(ElgatoEntity, LightEntity):
|
||||
self._attr_unique_id = coordinator.data.info.serial_number
|
||||
|
||||
# Elgato Light supporting color, have a different temperature range
|
||||
if self.coordinator.data.settings.power_on_hue is not None:
|
||||
if (
|
||||
self.coordinator.data.info.product_name
|
||||
in (
|
||||
"Elgato Light Strip",
|
||||
"Elgato Light Strip Pro",
|
||||
)
|
||||
or self.coordinator.data.settings.power_on_hue
|
||||
or self.coordinator.data.state.hue is not None
|
||||
):
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
self._attr_min_mireds = 153
|
||||
self._attr_max_mireds = 285
|
||||
|
@ -141,10 +141,10 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
self._device: OpenWebIfDevice = device
|
||||
self._entry = entry
|
||||
|
||||
self._attr_unique_id = device.mac_address
|
||||
self._attr_unique_id = device.mac_address or entry.entry_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.mac_address)},
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
manufacturer=about["info"]["brand"],
|
||||
model=about["info"]["model"],
|
||||
configuration_url=device.base,
|
||||
|
@ -116,8 +116,9 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
):
|
||||
"""Initialize the alarm panel."""
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
self._panic_type = panic_type
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._attr_code_format = CodeFormat.NUMBER
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
@ -141,13 +142,6 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
if partition is None or int(partition) == self._partition_number:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def code_format(self) -> CodeFormat | None:
|
||||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return CodeFormat.NUMBER
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Return the state of the device."""
|
||||
@ -169,34 +163,15 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].disarm_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_away_partition(code, self._partition_number)
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
"""Alarm trigger command. Will be used to trigger a panic alarm."""
|
||||
@ -204,9 +179,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
self.hass.data[DATA_EVL].arm_night_partition(
|
||||
str(code) if code else str(self._code), self._partition_number
|
||||
)
|
||||
self.hass.data[DATA_EVL].arm_night_partition(code, self._partition_number)
|
||||
|
||||
@callback
|
||||
def async_alarm_keypress(self, keypress=None):
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -71,7 +71,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
try:
|
||||
response = await model.generate_content_async(prompt_parts)
|
||||
except (
|
||||
ClientError,
|
||||
GoogleAPICallError,
|
||||
ValueError,
|
||||
genai_types.BlockedPromptException,
|
||||
genai_types.StopCandidateException,
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
from typing import Any, Literal
|
||||
|
||||
import google.ai.generativelanguage as glm
|
||||
from google.api_core.exceptions import GoogleAPICallError
|
||||
import google.generativeai as genai
|
||||
from google.generativeai import protos
|
||||
import google.generativeai.types as genai_types
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
import voluptuous as vol
|
||||
@ -93,7 +94,7 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
|
||||
parameters = _format_schema(convert(tool.parameters))
|
||||
|
||||
return glm.Tool(
|
||||
return protos.Tool(
|
||||
{
|
||||
"function_declarations": [
|
||||
{
|
||||
@ -106,14 +107,14 @@ def _format_tool(tool: llm.Tool) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
|
||||
def _adjust_value(value: Any) -> Any:
|
||||
"""Reverse unnecessary single quotes escaping."""
|
||||
def _escape_decode(value: Any) -> Any:
|
||||
"""Recursively call codecs.escape_decode on all values."""
|
||||
if isinstance(value, str):
|
||||
return value.replace("\\'", "'")
|
||||
return codecs.escape_decode(bytes(value, "utf-8"))[0].decode("utf-8") # type: ignore[attr-defined]
|
||||
if isinstance(value, list):
|
||||
return [_adjust_value(item) for item in value]
|
||||
return [_escape_decode(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {k: _adjust_value(v) for k, v in value.items()}
|
||||
return {k: _escape_decode(v) for k, v in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
@ -334,10 +335,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
for function_call in function_calls:
|
||||
tool_call = MessageToDict(function_call._pb) # noqa: SLF001
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = {
|
||||
key: _adjust_value(value)
|
||||
for key, value in tool_call["args"].items()
|
||||
}
|
||||
tool_args = _escape_decode(tool_call["args"])
|
||||
LOGGER.debug("Tool call: %s(%s)", tool_name, tool_args)
|
||||
tool_input = llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
try:
|
||||
@ -349,13 +347,13 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
LOGGER.debug("Tool response: %s", function_response)
|
||||
tool_responses.append(
|
||||
glm.Part(
|
||||
function_response=glm.FunctionResponse(
|
||||
protos.Part(
|
||||
function_response=protos.FunctionResponse(
|
||||
name=tool_name, response=function_response
|
||||
)
|
||||
)
|
||||
)
|
||||
chat_request = glm.Content(parts=tool_responses)
|
||||
chat_request = protos.Content(parts=tool_responses)
|
||||
|
||||
intent_response.async_set_speech(
|
||||
" ".join([part.text.strip() for part in chat_response.parts if part.text])
|
||||
|
@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-generativeai==0.5.4", "voluptuous-openapi==0.0.4"]
|
||||
"requirements": ["google-generativeai==0.6.0", "voluptuous-openapi==0.0.4"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==4.0.4"]
|
||||
"requirements": ["google-nest-sdm==4.0.5"]
|
||||
}
|
||||
|
@ -388,12 +388,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off the zone."""
|
||||
await self.async_set_hvac_mode(OPERATION_MODE_OFF)
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
self._signal_zone_update()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on the zone."""
|
||||
await self.async_set_hvac_mode(OPERATION_MODE_AUTO)
|
||||
await self.async_set_hvac_mode(HVACMode.AUTO)
|
||||
self._signal_zone_update()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
|
@ -73,7 +73,7 @@ def async_create_issue(hass: HomeAssistant, entry_id: str) -> None:
|
||||
domain=DOMAIN,
|
||||
issue_id=_get_issue_id(entry_id),
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
|
||||
translation_key="deprecated_v25",
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.6"]
|
||||
"requirements": ["opower==0.4.7"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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.",
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED
|
||||
from synology_dsm.exceptions import (
|
||||
SynologyDSMAPIErrorException,
|
||||
@ -40,7 +41,7 @@ DEFAULT_PORT = 5000
|
||||
DEFAULT_PORT_SSL = 5001
|
||||
# Options
|
||||
DEFAULT_SCAN_INTERVAL = 15 # min
|
||||
DEFAULT_TIMEOUT = 30 # sec
|
||||
DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15)
|
||||
DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
|
||||
|
||||
ENTITY_UNIT_LOAD = "load"
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.4.2"],
|
||||
"requirements": ["py-synologydsm-api==2.4.4"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
10
mypy.ini
10
mypy.ini
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"productName": "Elgato Key Light",
|
||||
"productName": "Elgato Light Strip",
|
||||
"hardwareBoardType": 53,
|
||||
"firmwareBuildNumber": 192,
|
||||
"firmwareVersion": "1.0.3",
|
||||
|
@ -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',
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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"},
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user