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.ecovacs.*
homeassistant.components.ecowitt.* homeassistant.components.ecowitt.*
homeassistant.components.efergy.* homeassistant.components.efergy.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.* homeassistant.components.electric_kiwi.*
homeassistant.components.elgato.* homeassistant.components.elgato.*
homeassistant.components.elkm1.* homeassistant.components.elkm1.*

View File

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

View File

@ -2,5 +2,5 @@
DOMAIN = "aladdin_connect" 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" 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. Adds an empty filter to hass data.
Tries to get a filter from yaml, if present set 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: 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 return True
@ -207,6 +206,6 @@ class AzureDataExplorer:
if "\n" in state.state: if "\n" in state.state:
return None, dropped + 1 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) return (json_event, dropped)

View File

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

View File

@ -10,6 +10,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlowResult from homeassistant.config_entries import ConfigFlowResult
from homeassistant.helpers.selector import BooleanSelector
from . import AzureDataExplorerClient from . import AzureDataExplorerClient
from .const import ( from .const import (
@ -19,7 +20,7 @@ from .const import (
CONF_APP_REG_ID, CONF_APP_REG_ID,
CONF_APP_REG_SECRET, CONF_APP_REG_SECRET,
CONF_AUTHORITY_ID, CONF_AUTHORITY_ID,
CONF_USE_FREE, CONF_USE_QUEUED_CLIENT,
DEFAULT_OPTIONS, DEFAULT_OPTIONS,
DOMAIN, DOMAIN,
) )
@ -34,7 +35,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_APP_REG_ID): str, vol.Required(CONF_APP_REG_ID): str,
vol.Required(CONF_APP_REG_SECRET): str, vol.Required(CONF_APP_REG_SECRET): str,
vol.Required(CONF_AUTHORITY_ID): 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_SEND_INTERVAL = "send_interval"
CONF_MAX_DELAY = "max_delay" CONF_MAX_DELAY = "max_delay"
CONF_FILTER = DATA_FILTER = "filter" CONF_FILTER = DATA_FILTER = "filter"
CONF_USE_FREE = "use_queued_ingestion" CONF_USE_QUEUED_CLIENT = "use_queued_ingestion"
DATA_HUB = "hub" DATA_HUB = "hub"
STEP_USER = "user" STEP_USER = "user"

View File

@ -3,15 +3,19 @@
"step": { "step": {
"user": { "user": {
"title": "Setup your Azure Data Explorer integration", "title": "Setup your Azure Data Explorer integration",
"description": "Enter connection details.", "description": "Enter connection details",
"data": { "data": {
"cluster_ingest_uri": "Cluster ingest URI", "cluster_ingest_uri": "Cluster Ingest URI",
"database": "Database name", "authority_id": "Authority ID",
"table": "Table name",
"client_id": "Client ID", "client_id": "Client ID",
"client_secret": "Client secret", "client_secret": "Client secret",
"authority_id": "Authority ID", "database": "Database name",
"table": "Table name",
"use_queued_ingestion": "Use queued ingestion" "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) director_all_items = json.loads(director_all_items)
entry_data[CONF_DIRECTOR_ALL_ITEMS] = 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 # Load options from config entry
entry_data[CONF_SCAN_INTERVAL] = entry.options.get( 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 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None: ) -> None:
"""Set up Control4 rooms from a config entry.""" """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) all_rooms = await get_rooms(hass, entry)
if not all_rooms: if not all_rooms:
return return
entry_data = hass.data[DOMAIN][entry.entry_id]
scan_interval = entry_data[CONF_SCAN_INTERVAL] scan_interval = entry_data[CONF_SCAN_INTERVAL]
_LOGGER.debug("Scan interval = %s", 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 if "parentId" in item and k > 1
} }
ui_config = entry_data[CONF_UI_CONFIGURATION]
entity_list = [] entity_list = []
for room in all_rooms: for room in all_rooms:
room_id = room["id"] room_id = room["id"]

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/electrasmart", "documentation": "https://www.home-assistant.io/integrations/electrasmart",
"iot_class": "cloud_polling", "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 self._attr_unique_id = coordinator.data.info.serial_number
# Elgato Light supporting color, have a different temperature range # 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_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
self._attr_min_mireds = 153 self._attr_min_mireds = 153
self._attr_max_mireds = 285 self._attr_max_mireds = 285

View File

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

View File

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

View File

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

View File

@ -13,5 +13,5 @@
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth", "documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling", "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", "documentation": "https://www.home-assistant.io/integrations/glances",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["glances_api"], "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 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): def sync_attributes(self):
"""Return ArmDisarm attributes for a sync request.""" """Return ArmDisarm attributes for a sync request."""
response = {} response = {}
@ -1609,10 +1620,13 @@ class ArmDisArmTrait(_Trait):
def query_attributes(self): def query_attributes(self):
"""Return ArmDisarm query attributes.""" """Return ArmDisarm query attributes."""
armed_state = self.state.attributes.get("next_state", self.state.state) armed_state = self.state.attributes.get("next_state", self.state.state)
response = {"isArmed": armed_state in self.state_to_service}
if response["isArmed"]: if armed_state in self.state_to_service:
response.update({"currentArmLevel": armed_state}) return {"isArmed": True, "currentArmLevel": armed_state}
return response return {
"isArmed": False,
"currentArmLevel": self._default_arm_state(),
}
async def execute(self, command, data, params, challenge): async def execute(self, command, data, params, challenge):
"""Execute an ArmDisarm command.""" """Execute an ArmDisarm command."""
@ -1620,15 +1634,7 @@ class ArmDisArmTrait(_Trait):
# If no arm level given, we can only arm it if there is # If no arm level given, we can only arm it if there is
# only one supported arm type. We never default to triggered. # only one supported arm type. We never default to triggered.
if not (arm_level := params.get("armLevel")): if not (arm_level := params.get("armLevel")):
states = self._supported_states() arm_level = self._default_arm_state()
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]
if self.state.state == arm_level: if self.state.state == arm_level:
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") 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: try:
response = await model.generate_content_async(prompt_parts) response = await model.generate_content_async(prompt_parts)
except ( except (
ClientError, GoogleAPICallError,
ValueError, ValueError,
genai_types.BlockedPromptException, genai_types.BlockedPromptException,
genai_types.StopCandidateException, genai_types.StopCandidateException,

View File

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

View File

@ -9,5 +9,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum", "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_UNAVAILABLE,
STATE_UNKNOWN, 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.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.entity import ( from homeassistant.helpers.entity import (
@ -45,6 +52,7 @@ from homeassistant.helpers.entity import (
get_unit_of_measurement, get_unit_of_measurement,
) )
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.issue_registry import ( from homeassistant.helpers.issue_registry import (
IssueSeverity, IssueSeverity,
async_create_issue, async_create_issue,
@ -329,6 +337,7 @@ class SensorGroup(GroupEntity, SensorEntity):
self._native_unit_of_measurement = unit_of_measurement self._native_unit_of_measurement = unit_of_measurement
self._valid_units: set[str | None] = set() self._valid_units: set[str | None] = set()
self._can_convert: bool = False self._can_convert: bool = False
self.calculate_attributes_later: CALLBACK_TYPE | None = None
self._attr_name = name self._attr_name = name
if name == DEFAULT_NAME: if name == DEFAULT_NAME:
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() 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: async def async_added_to_hass(self) -> None:
"""When added to hass.""" """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_state_class = self._calculate_state_class(self._state_class)
self._attr_device_class = self._calculate_device_class(self._device_class) self._attr_device_class = self._calculate_device_class(self._device_class)
self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement( self._attr_native_unit_of_measurement = self._calculate_unit_of_measurement(
self._native_unit_of_measurement self._native_unit_of_measurement
) )
self._valid_units = self._get_valid_units() self._valid_units = self._get_valid_units()
await super().async_added_to_hass()
@callback @callback
def async_update_group_state(self) -> None: def async_update_group_state(self) -> None:

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "platinum", "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", "iot_class": "cloud_push",
"loggers": ["google_nest_sdm"], "loggers": ["google_nest_sdm"],
"quality_scale": "platinum", "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: async def async_turn_off(self) -> None:
"""Turn off the zone.""" """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() self._signal_zone_update()
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn on the zone.""" """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() self._signal_zone_update()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: 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, domain=DOMAIN,
issue_id=_get_issue_id(entry_id), issue_id=_get_issue_id(entry_id),
is_fixable=True, is_fixable=True,
is_persistent=True, is_persistent=False,
severity=ir.IssueSeverity.WARNING, severity=ir.IssueSeverity.WARNING,
learn_more_url="https://www.home-assistant.io/integrations/openweathermap/", learn_more_url="https://www.home-assistant.io/integrations/openweathermap/",
translation_key="deprecated_v25", translation_key="deprecated_v25",

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower", "documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["opower"], "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 icmplib import NameLookupError, async_ping
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import ICMP_TIMEOUT, PING_TIMEOUT from .const import ICMP_TIMEOUT, PING_TIMEOUT
@ -58,9 +59,16 @@ class PingDataICMPLib(PingData):
timeout=ICMP_TIMEOUT, timeout=ICMP_TIMEOUT,
privileged=self._privileged, privileged=self._privileged,
) )
except NameLookupError: except NameLookupError as err:
self.is_alive = False 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 self.is_alive = data.is_alive
if not self.is_alive: if not self.is_alive:
@ -94,6 +102,10 @@ class PingDataSubProcess(PingData):
async def async_ping(self) -> dict[str, Any] | None: async def async_ping(self) -> dict[str, Any] | None:
"""Send ICMP echo request and return details if success.""" """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( pinger = await asyncio.create_subprocess_exec(
*self._ping_cmd, *self._ping_cmd,
stdin=None, stdin=None,
@ -140,20 +152,17 @@ class PingDataSubProcess(PingData):
if TYPE_CHECKING: if TYPE_CHECKING:
assert match is not None assert match is not None
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups() rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
except TimeoutError: except TimeoutError as err:
_LOGGER.exception(
"Timed out running command: `%s`, after: %ss",
self._ping_cmd,
self._count + PING_TIMEOUT,
)
if pinger: if pinger:
with suppress(TypeError): with suppress(TypeError):
await pinger.kill() # type: ignore[func-returns-value] await pinger.kill() # type: ignore[func-returns-value]
del pinger del pinger
return None raise UpdateFailed(
except AttributeError: f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s"
return None ) from err
except AttributeError as err:
raise UpdateFailed from err
return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev} return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
async def async_update(self) -> None: async def async_update(self) -> None:

View File

@ -1245,7 +1245,7 @@ def _first_statistic(
table: type[StatisticsBase], table: type[StatisticsBase],
metadata_id: int, metadata_id: int,
) -> datetime | None: ) -> 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( stmt = lambda_stmt(
lambda: select(table.start_ts) lambda: select(table.start_ts)
.filter(table.metadata_id == metadata_id) .filter(table.metadata_id == metadata_id)
@ -1257,12 +1257,30 @@ def _first_statistic(
return None 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( def _get_oldest_sum_statistic(
session: Session, session: Session,
head_start_time: datetime | None, head_start_time: datetime | None,
main_start_time: datetime | None, main_start_time: datetime | None,
tail_start_time: datetime | None, tail_start_time: datetime | None,
oldest_stat: datetime | None, oldest_stat: datetime | None,
oldest_5_min_stat: datetime | None,
tail_only: bool, tail_only: bool,
metadata_id: int, metadata_id: int,
) -> float | None: ) -> float | None:
@ -1307,6 +1325,15 @@ def _get_oldest_sum_statistic(
if ( if (
head_start_time is not None 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 ( and (
oldest_sum := _get_oldest_sum_statistic_in_sub_period( oldest_sum := _get_oldest_sum_statistic_in_sub_period(
session, head_start_time, StatisticsShortTerm, metadata_id session, head_start_time, StatisticsShortTerm, metadata_id
@ -1477,13 +1504,16 @@ def statistic_during_period(
tail_start_time: datetime | None = None tail_start_time: datetime | None = None
tail_end_time: datetime | None = None tail_end_time: datetime | None = None
if end_time is 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: elif end_time.minute:
tail_start_time = ( tail_start_time = end_time.replace(minute=0, second=0, microsecond=0)
start_time
if tail_only
else end_time.replace(minute=0, second=0, microsecond=0)
)
tail_end_time = end_time tail_end_time = end_time
# Calculate the main period # Calculate the main period
@ -1518,6 +1548,7 @@ def statistic_during_period(
main_start_time, main_start_time,
tail_start_time, tail_start_time,
oldest_stat, oldest_stat,
oldest_5_min_stat,
tail_only, tail_only,
metadata_id, metadata_id,
) )

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": [ "requirements": [
"python-roborock==2.2.3", "python-roborock==2.3.0",
"vacuum-map-parser-roborock==0.1.2" "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 version == 2:
if minor_version < 2: if minor_version < 2:
# Cleanup invalid MAC addresses - see #103512 # Cleanup invalid MAC addresses - see #103512
dev_reg = dr.async_get(hass) # Reverted due to device registry collisions - see #119082 / #119249
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
)
minor_version = 2 minor_version = 2
hass.config_entries.async_update_entry(config_entry, minor_version=2) hass.config_entries.async_update_entry(config_entry, minor_version=2)

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import contextlib
from typing import Final from typing import Final
from aioshelly.block_device import BlockDevice from aioshelly.block_device import BlockDevice
@ -301,13 +300,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> b
entry, platforms entry, platforms
): ):
if shelly_entry_data.rpc: if shelly_entry_data.rpc:
with contextlib.suppress(DeviceConnectionError): await shelly_entry_data.rpc.shutdown()
# 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()
return unload_ok return unload_ok

View File

@ -625,7 +625,13 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.connected: # Already connected if self.connected: # Already connected
return return
self.connected = True 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: async def _async_run_connected_events(self) -> None:
"""Run connected events. """Run connected events.
@ -699,10 +705,18 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
if self.device.connected: if self.device.connected:
try: try:
await async_stop_scanner(self.device) await async_stop_scanner(self.device)
await super().shutdown()
except InvalidAuthError: except InvalidAuthError:
self.entry.async_start_reauth(self.hass) self.entry.async_start_reauth(self.hass)
return 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) await self._async_disconnected(False)

View File

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

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_polling", "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 import logging
from aiohttp.client_exceptions import ServerDisconnectedError from aiohttp.client_exceptions import ServerDisconnectedError
from pyunifiprotect.data import Bootstrap from uiprotect.data import Bootstrap
from pyunifiprotect.data.types import FirmwareReleaseChannel from uiprotect.data.types import FirmwareReleaseChannel
from pyunifiprotect.exceptions import ClientError, NotAuthorized 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 # in __init__ to ensure it gets imported in the executor since the
# diagnostics module will not be imported in the executor. # 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.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any, cast 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.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ from enum import Enum
import logging import logging
from typing import TYPE_CHECKING, Any, Generic, TypeVar 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 from homeassistant.helpers.entity import EntityDescription

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/waqi", "documentation": "https://www.home-assistant.io/integrations/waqi",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aiowaqi"], "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: def _update_state_and_setup_listener(self) -> None:
"""Update state and setup listener for next interval.""" """Update state and setup listener for next interval."""
now = dt_util.utcnow() now = dt_util.now()
self.update_data(now) self.update_data(now)
self.unsub = async_track_point_in_utc_time( self.unsub = async_track_point_in_utc_time(
self.hass, self.point_in_time_listener, self.get_next_interval(now) 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" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024 MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 6 MINOR_VERSION: Final = 6
PATCH_VERSION: Final = "1" PATCH_VERSION: Final = "2"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) 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 abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from functools import cache, partial
from typing import Any from typing import Any
import slugify as unicode_slug
import voluptuous as vol import voluptuous as vol
from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE from homeassistant.components.climate.intent import INTENT_GET_TEMPERATURE
@ -175,10 +177,11 @@ class IntentTool(Tool):
def __init__( def __init__(
self, self,
name: str,
intent_handler: intent.IntentHandler, intent_handler: intent.IntentHandler,
) -> None: ) -> None:
"""Init the class.""" """Init the class."""
self.name = intent_handler.intent_type self.name = name
self.description = ( self.description = (
intent_handler.description or f"Execute Home Assistant {self.name} intent" intent_handler.description or f"Execute Home Assistant {self.name} intent"
) )
@ -261,6 +264,9 @@ class AssistAPI(API):
id=LLM_API_ASSIST, id=LLM_API_ASSIST,
name="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: async def async_get_api_instance(self, llm_context: LLMContext) -> APIInstance:
"""Return the instance of the API.""" """Return the instance of the API."""
@ -373,7 +379,10 @@ class AssistAPI(API):
or intent_handler.platforms & exposed_domains 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( 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 # runs before sleeping as otherwise if two runs are started at the exact
# same time they will cancel each other out. # same time they will cancel each other out.
self._log("Restarting") 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) await self.async_stop(update_state=False, spare=run)
if started_action: if started_action:

View File

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

View File

@ -1393,16 +1393,6 @@ disallow_untyped_defs = true
warn_return_any = true warn_return_any = true
warn_unreachable = 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.*] [mypy-homeassistant.components.electric_kiwi.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true

View File

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

View File

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

View File

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

View File

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

View File

@ -2771,6 +2771,7 @@ async def test_recursive_automation_starting_script(
], ],
"action": [ "action": [
{"service": "test.automation_started"}, {"service": "test.automation_started"},
{"delay": 0.001},
{"service": "script.script1"}, {"service": "script.script1"},
], ],
} }
@ -2817,7 +2818,10 @@ async def test_recursive_automation_starting_script(
assert script_warning_msg in caplog.text 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]) @pytest.mark.parametrize("wait_for_stop_scripts_after_shutdown", [True])
async def test_recursive_automation( async def test_recursive_automation(
hass: HomeAssistant, automation_mode, caplog: pytest.LogCaptureFixture hass: HomeAssistant, automation_mode, caplog: pytest.LogCaptureFixture
@ -2878,6 +2882,68 @@ async def test_recursive_automation(
assert "Disallowed recursion detected" not in caplog.text 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( async def test_websocket_config(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None: ) -> None:
@ -3097,3 +3163,72 @@ async def test_two_automations_call_restart_script_same_time(
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(events) == 2 assert len(events) == 2
cancel() 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_APP_REG_SECRET,
CONF_AUTHORITY_ID, CONF_AUTHORITY_ID,
CONF_SEND_INTERVAL, CONF_SEND_INTERVAL,
CONF_USE_FREE, CONF_USE_QUEUED_CLIENT,
) )
AZURE_DATA_EXPLORER_PATH = "homeassistant.components.azure_data_explorer" AZURE_DATA_EXPLORER_PATH = "homeassistant.components.azure_data_explorer"
@ -29,7 +29,7 @@ BASE_CONFIG_URI = {
} }
BASIC_OPTIONS = { BASIC_OPTIONS = {
CONF_USE_FREE: False, CONF_USE_QUEUED_CLIENT: False,
CONF_SEND_INTERVAL: 5, CONF_SEND_INTERVAL: 5,
} }
@ -39,10 +39,10 @@ BASE_CONFIG_FULL = BASE_CONFIG | BASIC_OPTIONS | BASE_CONFIG_URI
BASE_CONFIG_IMPORT = { BASE_CONFIG_IMPORT = {
CONF_ADX_CLUSTER_INGEST_URI: "https://cluster.region.kusto.windows.net", CONF_ADX_CLUSTER_INGEST_URI: "https://cluster.region.kusto.windows.net",
CONF_USE_FREE: False, CONF_USE_QUEUED_CLIENT: False,
CONF_SEND_INTERVAL: 5, 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 BASE_CONFIG_FREE = BASE_CONFIG | FREE_OPTIONS

View File

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

View File

@ -218,7 +218,7 @@
'labels': set({ 'labels': set({
}), }),
'manufacturer': 'Elgato', 'manufacturer': 'Elgato',
'model': 'Elgato Key Light', 'model': 'Elgato Light Strip',
'name': 'Frenck', 'name': 'Frenck',
'name_by_user': None, 'name_by_user': None,
'serial_number': 'CN11A1A00001', 'serial_number': 'CN11A1A00001',
@ -333,7 +333,7 @@
'labels': set({ 'labels': set({
}), }),
'manufacturer': 'Elgato', 'manufacturer': 'Elgato',
'model': 'Elgato Key Light', 'model': 'Elgato Light Strip',
'name': 'Frenck', 'name': 'Frenck',
'name_by_user': None, 'name_by_user': None,
'serial_number': 'CN11A1A00001', '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}) 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 import conversation
from homeassistant.components.conversation import trace 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.const import CONF_LLM_HASS_API
from homeassistant.core import Context, HomeAssistant from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -504,3 +507,18 @@ async def test_conversation_agent(
mock_config_entry.entry_id mock_config_entry.entry_id
) )
assert agent.supported_languages == "*" 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") state = hass.states.get("sensor.test_last")
assert str(float(value)) == state.state assert str(float(value)) == state.state
assert entity_id == state.attributes.get("last_entity_id") 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 result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address
assert len(mock_setup_entry.mock_calls) == 1 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 unittest.mock import ANY, patch
from freezegun import freeze_time from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from homeassistant.components import recorder 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.freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.UTC))
@pytest.mark.parametrize( @pytest.mark.parametrize(
("calendar_period", "start_time", "end_time"), ("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.usefixtures("remotews", "rest_api")
@pytest.mark.xfail
async def test_cleanup_mac( async def test_cleanup_mac(
hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion hass: HomeAssistant, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion
) -> None: ) -> None:
"""Test for `none` mac cleanup #103512.""" """Test for `none` mac cleanup #103512.
Reverted due to device registry collisions in #119249 / #119082
"""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=SAMSUNGTV_DOMAIN, domain=SAMSUNGTV_DOMAIN,
data=MOCK_ENTRY_WS_WITH_MAC, data=MOCK_ENTRY_WS_WITH_MAC,

View File

@ -15,12 +15,14 @@ from homeassistant.components.shelly.const import (
ATTR_CLICK_TYPE, ATTR_CLICK_TYPE,
ATTR_DEVICE, ATTR_DEVICE,
ATTR_GENERATION, ATTR_GENERATION,
CONF_BLE_SCANNER_MODE,
DOMAIN, DOMAIN,
ENTRY_RELOAD_COOLDOWN, ENTRY_RELOAD_COOLDOWN,
MAX_PUSH_UPDATE_FAILURES, MAX_PUSH_UPDATE_FAILURES,
RPC_RECONNECT_INTERVAL, RPC_RECONNECT_INTERVAL,
SLEEP_PERIOD_MULTIPLIER, SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER,
BLEScannerMode,
) )
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID, STATE_ON, STATE_UNAVAILABLE 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 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( async def test_rpc_click_event(
hass: HomeAssistant, hass: HomeAssistant,
mock_rpc_device: Mock, 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 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( async def test_rpc_polling_connection_error(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory, freezer: FrozenDateTimeFactory,

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock 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.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform 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 unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from uiprotect.data import Camera as ProtectCamera, CameraChannel, StateType
from pyunifiprotect.exceptions import NvrError from uiprotect.exceptions import NvrError
from homeassistant.components.camera import ( from homeassistant.components.camera import (
CameraEntityFeature, CameraEntityFeature,

View File

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

View File

@ -1,6 +1,6 @@
"""Test UniFi Protect diagnostics.""" """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.components.unifiprotect.const import CONF_ALLOW_EA
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

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

View File

@ -4,8 +4,8 @@ from __future__ import annotations
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
from pyunifiprotect.data import Light from uiprotect.data import Light
from pyunifiprotect.data.types import LEDLevel from uiprotect.data.types import LEDLevel
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION 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 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.components.unifiprotect.const import DEFAULT_ATTRIBUTION
from homeassistant.const import ( from homeassistant.const import (

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import patch 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.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.repairs.issue_handler import ( from homeassistant.components.repairs.issue_handler import (

View File

@ -6,7 +6,7 @@ from datetime import timedelta
from unittest.mock import AsyncMock, Mock from unittest.mock import AsyncMock, Mock
import pytest 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.const import DEFAULT_ATTRIBUTION
from homeassistant.components.unifiprotect.number import ( from homeassistant.components.unifiprotect.number import (

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import Mock 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 import Recorder
from homeassistant.components.recorder.history import get_significant_states 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 http import HTTPStatus
from unittest.mock import AsyncMock, Mock 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 ( from homeassistant.components.repairs.issue_handler import (
async_process_repairs_platforms, async_process_repairs_platforms,

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