mirror of
https://github.com/home-assistant/core.git
synced 2025-07-30 08:47:09 +00:00
Merge branch 'dev' into dev
This commit is contained in:
commit
8d1dc732c9
@ -1106,13 +1106,12 @@ class PipelineRun:
|
|||||||
speech: str = intent_response.speech.get("plain", {}).get(
|
speech: str = intent_response.speech.get("plain", {}).get(
|
||||||
"speech", ""
|
"speech", ""
|
||||||
)
|
)
|
||||||
async for _ in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
conversation.AssistantContent(
|
conversation.AssistantContent(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
content=speech,
|
content=speech,
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pass
|
|
||||||
conversation_result = conversation.ConversationResult(
|
conversation_result = conversation.ConversationResult(
|
||||||
response=intent_response,
|
response=intent_response,
|
||||||
conversation_id=session.conversation_id,
|
conversation_id=session.conversation_id,
|
||||||
|
@ -265,12 +265,11 @@ class AssistSatelliteEntity(entity.Entity):
|
|||||||
self._conversation_id = session.conversation_id
|
self._conversation_id = session.conversation_id
|
||||||
|
|
||||||
if start_message:
|
if start_message:
|
||||||
async for _tool_response in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
conversation.AssistantContent(
|
conversation.AssistantContent(
|
||||||
agent_id=self.entity_id, content=start_message
|
agent_id=self.entity_id, content=start_message
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pass # no tool responses.
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.async_start_conversation(announcement)
|
await self.async_start_conversation(announcement)
|
||||||
|
@ -143,6 +143,15 @@ class ChatLog:
|
|||||||
"""Add user content to the log."""
|
"""Add user content to the log."""
|
||||||
self.content.append(content)
|
self.content.append(content)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_add_assistant_content_without_tools(
|
||||||
|
self, content: AssistantContent
|
||||||
|
) -> None:
|
||||||
|
"""Add assistant content to the log."""
|
||||||
|
if content.tool_calls is not None:
|
||||||
|
raise ValueError("Tool calls not allowed")
|
||||||
|
self.content.append(content)
|
||||||
|
|
||||||
async def async_add_assistant_content(
|
async def async_add_assistant_content(
|
||||||
self, content: AssistantContent
|
self, content: AssistantContent
|
||||||
) -> AsyncGenerator[ToolResultContent]:
|
) -> AsyncGenerator[ToolResultContent]:
|
||||||
|
@ -379,13 +379,12 @@ class DefaultAgent(ConversationEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
speech: str = response.speech.get("plain", {}).get("speech", "")
|
speech: str = response.speech.get("plain", {}).get("speech", "")
|
||||||
async for _tool_result in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
AssistantContent(
|
AssistantContent(
|
||||||
agent_id=user_input.agent_id, # type: ignore[arg-type]
|
agent_id=user_input.agent_id, # type: ignore[arg-type]
|
||||||
content=speech,
|
content=speech,
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pass
|
|
||||||
|
|
||||||
return ConversationResult(
|
return ConversationResult(
|
||||||
response=response, conversation_id=session.conversation_id
|
response=response, conversation_id=session.conversation_id
|
||||||
|
@ -21,5 +21,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==20250131.0"]
|
"requirements": ["home-assistant-frontend==20250203.0"]
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ class IdasenDeskConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if discovery := self._discovery_info:
|
if discovery := self._discovery_info:
|
||||||
self._discovered_devices[discovery.address] = discovery
|
self._discovered_devices[discovery.address] = discovery
|
||||||
else:
|
else:
|
||||||
current_addresses = self._async_current_ids()
|
current_addresses = self._async_current_ids(include_ignore=False)
|
||||||
for discovery in async_discovered_service_info(self.hass):
|
for discovery in async_discovered_service_info(self.hass):
|
||||||
if (
|
if (
|
||||||
discovery.address in current_addresses
|
discovery.address in current_addresses
|
||||||
|
@ -116,6 +116,10 @@ BATTERY_DEVICES_WITH_PERMANENT_CONNECTION: Final = [
|
|||||||
# Button/Click events for Block & RPC devices
|
# Button/Click events for Block & RPC devices
|
||||||
EVENT_SHELLY_CLICK: Final = "shelly.click"
|
EVENT_SHELLY_CLICK: Final = "shelly.click"
|
||||||
|
|
||||||
|
SHELLY_EMIT_EVENT_PATTERN: Final = re.compile(
|
||||||
|
r"(?:Shelly\s*\.\s*emitEvent\s*\(\s*[\"'`])(\w*)"
|
||||||
|
)
|
||||||
|
|
||||||
ATTR_CLICK_TYPE: Final = "click_type"
|
ATTR_CLICK_TYPE: Final = "click_type"
|
||||||
ATTR_CHANNEL: Final = "channel"
|
ATTR_CHANNEL: Final = "channel"
|
||||||
ATTR_DEVICE: Final = "device"
|
ATTR_DEVICE: Final = "device"
|
||||||
|
@ -6,6 +6,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
|
from aioshelly.ble.const import BLE_SCRIPT_NAME
|
||||||
from aioshelly.block_device import Block
|
from aioshelly.block_device import Block
|
||||||
from aioshelly.const import MODEL_I3, RPC_GENERATIONS
|
from aioshelly.const import MODEL_I3, RPC_GENERATIONS
|
||||||
|
|
||||||
@ -28,10 +29,12 @@ from .const import (
|
|||||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||||
from .entity import ShellyBlockEntity
|
from .entity import ShellyBlockEntity
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
async_remove_orphaned_entities,
|
||||||
async_remove_shelly_entity,
|
async_remove_shelly_entity,
|
||||||
get_device_entry_gen,
|
get_device_entry_gen,
|
||||||
get_rpc_entity_name,
|
get_rpc_entity_name,
|
||||||
get_rpc_key_instances,
|
get_rpc_key_instances,
|
||||||
|
get_rpc_script_event_types,
|
||||||
is_block_momentary_input,
|
is_block_momentary_input,
|
||||||
is_rpc_momentary_input,
|
is_rpc_momentary_input,
|
||||||
)
|
)
|
||||||
@ -68,6 +71,13 @@ RPC_EVENT: Final = ShellyRpcEventDescription(
|
|||||||
config, status, key
|
config, status, key
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
SCRIPT_EVENT: Final = ShellyRpcEventDescription(
|
||||||
|
key="script",
|
||||||
|
translation_key="script",
|
||||||
|
device_class=None,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
has_entity_name=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -95,6 +105,33 @@ async def async_setup_entry(
|
|||||||
async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id)
|
async_remove_shelly_entity(hass, EVENT_DOMAIN, unique_id)
|
||||||
else:
|
else:
|
||||||
entities.append(ShellyRpcEvent(coordinator, key, RPC_EVENT))
|
entities.append(ShellyRpcEvent(coordinator, key, RPC_EVENT))
|
||||||
|
|
||||||
|
script_instances = get_rpc_key_instances(
|
||||||
|
coordinator.device.status, SCRIPT_EVENT.key
|
||||||
|
)
|
||||||
|
for script in script_instances:
|
||||||
|
script_name = get_rpc_entity_name(coordinator.device, script)
|
||||||
|
if script_name == BLE_SCRIPT_NAME:
|
||||||
|
continue
|
||||||
|
|
||||||
|
event_types = await get_rpc_script_event_types(
|
||||||
|
coordinator.device, int(script.split(":")[-1])
|
||||||
|
)
|
||||||
|
if not event_types:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entities.append(ShellyRpcScriptEvent(coordinator, script, event_types))
|
||||||
|
|
||||||
|
# If a script is removed, from the device configuration, we need to remove orphaned entities
|
||||||
|
async_remove_orphaned_entities(
|
||||||
|
hass,
|
||||||
|
config_entry.entry_id,
|
||||||
|
coordinator.mac,
|
||||||
|
EVENT_DOMAIN,
|
||||||
|
coordinator.device.status,
|
||||||
|
"script",
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
coordinator = config_entry.runtime_data.block
|
coordinator = config_entry.runtime_data.block
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -170,7 +207,7 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize Shelly entity."""
|
"""Initialize Shelly entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.input_index = int(key.split(":")[-1])
|
self.event_id = int(key.split(":")[-1])
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
|
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)}
|
||||||
)
|
)
|
||||||
@ -181,6 +218,7 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
|
|||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
self.coordinator.async_subscribe_input_events(self._async_handle_event)
|
self.coordinator.async_subscribe_input_events(self._async_handle_event)
|
||||||
)
|
)
|
||||||
@ -188,6 +226,42 @@ class ShellyRpcEvent(CoordinatorEntity[ShellyRpcCoordinator], EventEntity):
|
|||||||
@callback
|
@callback
|
||||||
def _async_handle_event(self, event: dict[str, Any]) -> None:
|
def _async_handle_event(self, event: dict[str, Any]) -> None:
|
||||||
"""Handle the demo button event."""
|
"""Handle the demo button event."""
|
||||||
if event["id"] == self.input_index:
|
if event["id"] == self.event_id:
|
||||||
self._trigger_event(event["event"])
|
self._trigger_event(event["event"])
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ShellyRpcScriptEvent(ShellyRpcEvent):
|
||||||
|
"""Represent RPC script event entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: ShellyRpcCoordinator,
|
||||||
|
key: str,
|
||||||
|
event_types: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Shelly script event entity."""
|
||||||
|
super().__init__(coordinator, key, SCRIPT_EVENT)
|
||||||
|
|
||||||
|
self.component = key
|
||||||
|
self._attr_event_types = event_types
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""When entity is added to hass."""
|
||||||
|
await super(CoordinatorEntity, self).async_added_to_hass()
|
||||||
|
|
||||||
|
self.async_on_remove(
|
||||||
|
self.coordinator.async_subscribe_events(self._async_handle_event)
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_handle_event(self, event: dict[str, Any]) -> None:
|
||||||
|
"""Handle script event."""
|
||||||
|
if event.get("component") == self.component:
|
||||||
|
event_type = event.get("event")
|
||||||
|
if event_type not in self.event_types:
|
||||||
|
# This can happen if we didn't find this event type in the script
|
||||||
|
return
|
||||||
|
|
||||||
|
self._trigger_event(event_type, event.get("data"))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
@ -56,6 +56,7 @@ from .const import (
|
|||||||
RPC_INPUTS_EVENTS_TYPES,
|
RPC_INPUTS_EVENTS_TYPES,
|
||||||
SHBTN_INPUTS_EVENTS_TYPES,
|
SHBTN_INPUTS_EVENTS_TYPES,
|
||||||
SHBTN_MODELS,
|
SHBTN_MODELS,
|
||||||
|
SHELLY_EMIT_EVENT_PATTERN,
|
||||||
SHIX3_1_INPUTS_EVENTS_TYPES,
|
SHIX3_1_INPUTS_EVENTS_TYPES,
|
||||||
UPTIME_DEVIATION,
|
UPTIME_DEVIATION,
|
||||||
VIRTUAL_COMPONENTS_MAP,
|
VIRTUAL_COMPONENTS_MAP,
|
||||||
@ -598,3 +599,10 @@ def get_rpc_ws_url(hass: HomeAssistant) -> str | None:
|
|||||||
url = URL(raw_url)
|
url = URL(raw_url)
|
||||||
ws_url = url.with_scheme("wss" if url.scheme == "https" else "ws")
|
ws_url = url.with_scheme("wss" if url.scheme == "https" else "ws")
|
||||||
return str(ws_url.joinpath(API_WS_URL.removeprefix("/")))
|
return str(ws_url.joinpath(API_WS_URL.removeprefix("/")))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_rpc_script_event_types(device: RpcDevice, id: int) -> list[str]:
|
||||||
|
"""Return a list of event types for a specific script."""
|
||||||
|
code_response = await device.script_getcode(id)
|
||||||
|
matches = SHELLY_EMIT_EVENT_PATTERN.finditer(code_response["data"])
|
||||||
|
return sorted([*{str(event_type.group(1)) for event_type in matches}])
|
||||||
|
@ -8,13 +8,16 @@ from typing import Any
|
|||||||
from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions
|
from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME
|
from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
|
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
|
||||||
|
from .helpers import cleanup_device_tracker
|
||||||
|
|
||||||
CONSIDER_HOME_SECONDS = DEFAULT_CONSIDER_HOME.total_seconds()
|
CONSIDER_HOME_SECONDS = DEFAULT_CONSIDER_HOME.total_seconds()
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ class UpdateCoordinatorDataType:
|
|||||||
class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||||
"""Queries router running Vodafone Station firmware."""
|
"""Queries router running Vodafone Station firmware."""
|
||||||
|
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -61,6 +66,17 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
name=f"{DOMAIN}-{host}-coordinator",
|
name=f"{DOMAIN}-{host}-coordinator",
|
||||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
|
device_reg = dr.async_get(self.hass)
|
||||||
|
device_list = dr.async_entries_for_config_entry(
|
||||||
|
device_reg, self.config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_devices = {
|
||||||
|
connection[1].upper()
|
||||||
|
for device in device_list
|
||||||
|
for connection in device.connections
|
||||||
|
if connection[0] == dr.CONNECTION_NETWORK_MAC
|
||||||
|
}
|
||||||
|
|
||||||
def _calculate_update_time_and_consider_home(
|
def _calculate_update_time_and_consider_home(
|
||||||
self, device: VodafoneStationDevice, utc_point_in_time: datetime
|
self, device: VodafoneStationDevice, utc_point_in_time: datetime
|
||||||
@ -125,6 +141,18 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
|||||||
)
|
)
|
||||||
for dev_info in (raw_data_devices).values()
|
for dev_info in (raw_data_devices).values()
|
||||||
}
|
}
|
||||||
|
current_devices = set(data_devices)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Loaded current %s devices: %s", len(current_devices), current_devices
|
||||||
|
)
|
||||||
|
if stale_devices := self.previous_devices - current_devices:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Found %s stale devices: %s", len(stale_devices), stale_devices
|
||||||
|
)
|
||||||
|
await cleanup_device_tracker(self.hass, self.config_entry, data_devices)
|
||||||
|
|
||||||
|
self.previous_devices = current_devices
|
||||||
|
|
||||||
return UpdateCoordinatorDataType(data_devices, data_sensors)
|
return UpdateCoordinatorDataType(data_devices, data_sensors)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -61,6 +61,7 @@ class VodafoneStationTracker(CoordinatorEntity[VodafoneStationRouter], ScannerEn
|
|||||||
"""Representation of a Vodafone Station device."""
|
"""Representation of a Vodafone Station device."""
|
||||||
|
|
||||||
_attr_translation_key = "device_tracker"
|
_attr_translation_key = "device_tracker"
|
||||||
|
_attr_has_entity_name = True
|
||||||
mac_address: str
|
mac_address: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -72,7 +73,9 @@ class VodafoneStationTracker(CoordinatorEntity[VodafoneStationRouter], ScannerEn
|
|||||||
mac = device_info.device.mac
|
mac = device_info.device.mac
|
||||||
self._attr_mac_address = mac
|
self._attr_mac_address = mac
|
||||||
self._attr_unique_id = mac
|
self._attr_unique_id = mac
|
||||||
self._attr_hostname = device_info.device.name or mac.replace(":", "_")
|
self._attr_hostname = self._attr_name = device_info.device.name or mac.replace(
|
||||||
|
":", "_"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _device_info(self) -> VodafoneStationDeviceInfo:
|
def _device_info(self) -> VodafoneStationDeviceInfo:
|
||||||
|
72
homeassistant/components/vodafone_station/helpers.py
Normal file
72
homeassistant/components/vodafone_station/helpers.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
"""Vodafone Station helpers."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
|
from .const import _LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_device_tracker(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry, devices: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Cleanup stale device tracker."""
|
||||||
|
entity_reg: er.EntityRegistry = er.async_get(hass)
|
||||||
|
|
||||||
|
entities_removed: bool = False
|
||||||
|
|
||||||
|
device_hosts_macs: set[str] = set()
|
||||||
|
device_hosts_names: set[str] = set()
|
||||||
|
for mac, device_info in devices.items():
|
||||||
|
device_hosts_macs.add(mac)
|
||||||
|
device_hosts_names.add(device_info.device.name)
|
||||||
|
|
||||||
|
for entry in er.async_entries_for_config_entry(entity_reg, config_entry.entry_id):
|
||||||
|
if entry.domain != DEVICE_TRACKER_DOMAIN:
|
||||||
|
continue
|
||||||
|
entry_name = entry.name or entry.original_name
|
||||||
|
entry_host = entry_name.partition(" ")[0] if entry_name else None
|
||||||
|
entry_mac = entry.unique_id.partition("_")[0]
|
||||||
|
|
||||||
|
# Some devices, mainly routers, allow to change the hostname of the connected devices.
|
||||||
|
# This can lead to entities no longer aligned to the device UI
|
||||||
|
if (
|
||||||
|
entry_host
|
||||||
|
and entry_host in device_hosts_names
|
||||||
|
and entry_mac in device_hosts_macs
|
||||||
|
):
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Skipping entity %s [mac=%s, host=%s]",
|
||||||
|
entry_name,
|
||||||
|
entry_mac,
|
||||||
|
entry_host,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# Entity is removed so that at the next coordinator update
|
||||||
|
# the correct one will be created
|
||||||
|
_LOGGER.info("Removing entity: %s", entry_name)
|
||||||
|
entity_reg.async_remove(entry.entity_id)
|
||||||
|
entities_removed = True
|
||||||
|
|
||||||
|
if entities_removed:
|
||||||
|
_async_remove_empty_devices(hass, entity_reg, config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
def _async_remove_empty_devices(
|
||||||
|
hass: HomeAssistant, entity_reg: er.EntityRegistry, config_entry: ConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Remove devices with no entities."""
|
||||||
|
|
||||||
|
device_reg = dr.async_get(hass)
|
||||||
|
device_list = dr.async_entries_for_config_entry(device_reg, config_entry.entry_id)
|
||||||
|
for device_entry in device_list:
|
||||||
|
if not er.async_entries_for_device(
|
||||||
|
entity_reg,
|
||||||
|
device_entry.id,
|
||||||
|
include_disabled_entities=True,
|
||||||
|
):
|
||||||
|
_LOGGER.info("Removing device: %s", device_entry.name)
|
||||||
|
device_reg.async_remove_device(device_entry.id)
|
@ -37,7 +37,7 @@ habluetooth==3.21.0
|
|||||||
hass-nabucasa==0.88.1
|
hass-nabucasa==0.88.1
|
||||||
hassil==2.2.0
|
hassil==2.2.0
|
||||||
home-assistant-bluetooth==1.13.0
|
home-assistant-bluetooth==1.13.0
|
||||||
home-assistant-frontend==20250131.0
|
home-assistant-frontend==20250203.0
|
||||||
home-assistant-intents==2025.1.28
|
home-assistant-intents==2025.1.28
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@ -1149,7 +1149,7 @@ hole==0.8.0
|
|||||||
holidays==0.65
|
holidays==0.65
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250131.0
|
home-assistant-frontend==20250203.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.1.28
|
home-assistant-intents==2025.1.28
|
||||||
|
@ -33,7 +33,7 @@ pytest==8.3.4
|
|||||||
requests-mock==1.12.1
|
requests-mock==1.12.1
|
||||||
respx==0.22.0
|
respx==0.22.0
|
||||||
syrupy==4.8.1
|
syrupy==4.8.1
|
||||||
tqdm==4.66.5
|
tqdm==4.67.1
|
||||||
types-aiofiles==24.1.0.20241221
|
types-aiofiles==24.1.0.20241221
|
||||||
types-atomicwrites==1.4.5.1
|
types-atomicwrites==1.4.5.1
|
||||||
types-croniter==5.0.1.20241205
|
types-croniter==5.0.1.20241205
|
||||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@ -975,7 +975,7 @@ hole==0.8.0
|
|||||||
holidays==0.65
|
holidays==0.65
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20250131.0
|
home-assistant-frontend==20250203.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2025.1.28
|
home-assistant-intents==2025.1.28
|
||||||
|
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@ -24,7 +24,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.21,source=/uv,target=/bin/uv \
|
|||||||
--no-cache \
|
--no-cache \
|
||||||
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
||||||
-r /usr/src/homeassistant/requirements.txt \
|
-r /usr/src/homeassistant/requirements.txt \
|
||||||
stdlib-list==0.10.0 pipdeptree==2.25.0 tqdm==4.66.5 ruff==0.9.1 \
|
stdlib-list==0.10.0 pipdeptree==2.25.0 tqdm==4.67.1 ruff==0.9.1 \
|
||||||
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.0 home-assistant-intents==2025.1.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.0 home-assistant-intents==2025.1.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
||||||
|
|
||||||
LABEL "name"="hassfest"
|
LABEL "name"="hassfest"
|
||||||
|
@ -56,13 +56,12 @@ async def test_cleanup(
|
|||||||
):
|
):
|
||||||
conversation_id = session.conversation_id
|
conversation_id = session.conversation_id
|
||||||
# Add message so it persists
|
# Add message so it persists
|
||||||
async for _tool_result in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
AssistantContent(
|
AssistantContent(
|
||||||
agent_id="mock-agent-id",
|
agent_id="mock-agent-id",
|
||||||
content="Hey!",
|
content="Hey!",
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pytest.fail("should not reach here")
|
|
||||||
|
|
||||||
assert conversation_id in hass.data[DATA_CHAT_HISTORY]
|
assert conversation_id in hass.data[DATA_CHAT_HISTORY]
|
||||||
|
|
||||||
@ -210,13 +209,12 @@ async def test_extra_systen_prompt(
|
|||||||
user_llm_hass_api=None,
|
user_llm_hass_api=None,
|
||||||
user_llm_prompt=None,
|
user_llm_prompt=None,
|
||||||
)
|
)
|
||||||
async for _tool_result in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
AssistantContent(
|
AssistantContent(
|
||||||
agent_id="mock-agent-id",
|
agent_id="mock-agent-id",
|
||||||
content="Hey!",
|
content="Hey!",
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pytest.fail("should not reach here")
|
|
||||||
|
|
||||||
assert chat_log.extra_system_prompt == extra_system_prompt
|
assert chat_log.extra_system_prompt == extra_system_prompt
|
||||||
assert chat_log.content[0].content.endswith(extra_system_prompt)
|
assert chat_log.content[0].content.endswith(extra_system_prompt)
|
||||||
@ -252,13 +250,12 @@ async def test_extra_systen_prompt(
|
|||||||
user_llm_hass_api=None,
|
user_llm_hass_api=None,
|
||||||
user_llm_prompt=None,
|
user_llm_prompt=None,
|
||||||
)
|
)
|
||||||
async for _tool_result in chat_log.async_add_assistant_content(
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
AssistantContent(
|
AssistantContent(
|
||||||
agent_id="mock-agent-id",
|
agent_id="mock-agent-id",
|
||||||
content="Hey!",
|
content="Hey!",
|
||||||
)
|
)
|
||||||
):
|
)
|
||||||
pytest.fail("should not reach here")
|
|
||||||
|
|
||||||
assert chat_log.extra_system_prompt == extra_system_prompt2
|
assert chat_log.extra_system_prompt == extra_system_prompt2
|
||||||
assert chat_log.content[0].content.endswith(extra_system_prompt2)
|
assert chat_log.content[0].content.endswith(extra_system_prompt2)
|
||||||
@ -311,19 +308,24 @@ async def test_tool_call(
|
|||||||
user_llm_hass_api="assist",
|
user_llm_hass_api="assist",
|
||||||
user_llm_prompt=None,
|
user_llm_prompt=None,
|
||||||
)
|
)
|
||||||
|
content = AssistantContent(
|
||||||
|
agent_id=mock_conversation_input.agent_id,
|
||||||
|
content="",
|
||||||
|
tool_calls=[
|
||||||
|
llm.ToolInput(
|
||||||
|
id="mock-tool-call-id",
|
||||||
|
tool_name="test_tool",
|
||||||
|
tool_args={"param1": "Test Param"},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
chat_log.async_add_assistant_content_without_tools(content)
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
async for tool_result_content in chat_log.async_add_assistant_content(
|
async for tool_result_content in chat_log.async_add_assistant_content(
|
||||||
AssistantContent(
|
content
|
||||||
agent_id=mock_conversation_input.agent_id,
|
|
||||||
content="",
|
|
||||||
tool_calls=[
|
|
||||||
llm.ToolInput(
|
|
||||||
id="mock-tool-call-id",
|
|
||||||
tool_name="test_tool",
|
|
||||||
tool_args={"param1": "Test Param"},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
assert result is None
|
assert result is None
|
||||||
result = tool_result_content
|
result = tool_result_content
|
||||||
|
@ -50,6 +50,49 @@ async def test_user_step_success(hass: HomeAssistant, mock_desk_api: MagicMock)
|
|||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_step_replaces_ignored_device(
|
||||||
|
hass: HomeAssistant, mock_desk_api: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test user step replaces ignored devices."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id=IDASEN_DISCOVERY_INFO.address,
|
||||||
|
source=config_entries.SOURCE_IGNORE,
|
||||||
|
data={CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||||
|
return_value=[NOT_IDASEN_DISCOVERY_INFO, IDASEN_DISCOVERY_INFO],
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.idasen_desk.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == IDASEN_DISCOVERY_INFO.name
|
||||||
|
assert result2["data"] == {
|
||||||
|
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||||
|
}
|
||||||
|
assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
|
async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
|
||||||
"""Test user step with no devices found."""
|
"""Test user step with no devices found."""
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
||||||
|
|
||||||
|
from aioshelly.ble.const import (
|
||||||
|
BLE_CODE,
|
||||||
|
BLE_SCAN_RESULT_EVENT,
|
||||||
|
BLE_SCAN_RESULT_VERSION,
|
||||||
|
BLE_SCRIPT_NAME,
|
||||||
|
VAR_ACTIVE,
|
||||||
|
VAR_EVENT_TYPE,
|
||||||
|
VAR_VERSION,
|
||||||
|
)
|
||||||
from aioshelly.block_device import BlockDevice, BlockUpdateType
|
from aioshelly.block_device import BlockDevice, BlockUpdateType
|
||||||
from aioshelly.const import MODEL_1, MODEL_25, MODEL_PLUS_2PM
|
from aioshelly.const import MODEL_1, MODEL_25, MODEL_PLUS_2PM
|
||||||
from aioshelly.rpc_device import RpcDevice, RpcUpdateType
|
from aioshelly.rpc_device import RpcDevice, RpcUpdateType
|
||||||
@ -201,6 +210,9 @@ MOCK_CONFIG = {
|
|||||||
"wifi": {"sta": {"enable": True}, "sta1": {"enable": False}},
|
"wifi": {"sta": {"enable": True}, "sta1": {"enable": False}},
|
||||||
"ws": {"enable": False, "server": None},
|
"ws": {"enable": False, "server": None},
|
||||||
"voltmeter:100": {"xvoltage": {"unit": "ppm"}},
|
"voltmeter:100": {"xvoltage": {"unit": "ppm"}},
|
||||||
|
"script:1": {"id": 1, "name": "test_script.js", "enable": True},
|
||||||
|
"script:2": {"id": 2, "name": "test_script_2.js", "enable": False},
|
||||||
|
"script:3": {"id": 3, "name": BLE_SCRIPT_NAME, "enable": False},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -335,6 +347,15 @@ MOCK_STATUS_RPC = {
|
|||||||
"current_C": 12.3,
|
"current_C": 12.3,
|
||||||
"output": True,
|
"output": True,
|
||||||
},
|
},
|
||||||
|
"script:1": {
|
||||||
|
"id": 1,
|
||||||
|
"running": True,
|
||||||
|
"mem_used": 826,
|
||||||
|
"mem_peak": 1666,
|
||||||
|
"mem_free": 24360,
|
||||||
|
},
|
||||||
|
"script:2": {"id": 2, "running": False},
|
||||||
|
"script:3": {"id": 3, "running": False},
|
||||||
"humidity:0": {"rh": 44.4},
|
"humidity:0": {"rh": 44.4},
|
||||||
"sys": {
|
"sys": {
|
||||||
"available_updates": {
|
"available_updates": {
|
||||||
@ -347,6 +368,28 @@ MOCK_STATUS_RPC = {
|
|||||||
"wifi": {"rssi": -63},
|
"wifi": {"rssi": -63},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MOCK_SCRIPTS = [
|
||||||
|
""""
|
||||||
|
function eventHandler(event, userdata) {
|
||||||
|
if (typeof event.component !== "string")
|
||||||
|
return;
|
||||||
|
|
||||||
|
let component = event.component.substring(0, 5);
|
||||||
|
if (component === "input") {
|
||||||
|
let id = Number(event.component.substring(6));
|
||||||
|
Shelly.emitEvent("input_event", { id: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Shelly.addEventHandler(eventHandler);
|
||||||
|
Shelly.emitEvent("script_start");
|
||||||
|
""",
|
||||||
|
'console.log("Hello World!")',
|
||||||
|
BLE_CODE.replace(VAR_ACTIVE, "true")
|
||||||
|
.replace(VAR_EVENT_TYPE, BLE_SCAN_RESULT_EVENT)
|
||||||
|
.replace(VAR_VERSION, str(BLE_SCAN_RESULT_VERSION)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_coap():
|
def mock_coap():
|
||||||
@ -430,6 +473,9 @@ def _mock_rpc_device(version: str | None = None):
|
|||||||
firmware_version="some fw string",
|
firmware_version="some fw string",
|
||||||
initialized=True,
|
initialized=True,
|
||||||
connected=True,
|
connected=True,
|
||||||
|
script_getcode=AsyncMock(
|
||||||
|
side_effect=lambda script_id: {"data": MOCK_SCRIPTS[script_id - 1]}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
type(device).name = PropertyMock(return_value="Test name")
|
type(device).name = PropertyMock(return_value="Test name")
|
||||||
return device
|
return device
|
||||||
|
69
tests/components/shelly/snapshots/test_event.ambr
Normal file
69
tests/components/shelly/snapshots/test_event.ambr
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_rpc_script_1_event[event.test_name_test_script_js-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'event_types': list([
|
||||||
|
'input_event',
|
||||||
|
'script_start',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'event',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'event.test_name_test_script_js',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'test_script.js',
|
||||||
|
'platform': 'shelly',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'script',
|
||||||
|
'unique_id': '123456789ABC-script:1',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_rpc_script_1_event[event.test_name_test_script_js-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'event_type': None,
|
||||||
|
'event_types': list([
|
||||||
|
'input_event',
|
||||||
|
'script_start',
|
||||||
|
]),
|
||||||
|
'friendly_name': 'Test name test_script.js',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'event.test_name_test_script_js',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'unknown',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_rpc_script_2_event[event.test_name_test_script_2_js-entry]
|
||||||
|
None
|
||||||
|
# ---
|
||||||
|
# name: test_rpc_script_2_event[event.test_name_test_script_2_js-state]
|
||||||
|
None
|
||||||
|
# ---
|
||||||
|
# name: test_rpc_script_ble_event[event.test_name_aioshelly_ble_integration-entry]
|
||||||
|
None
|
||||||
|
# ---
|
||||||
|
# name: test_rpc_script_ble_event[event.test_name_aioshelly_ble_integration-state]
|
||||||
|
None
|
||||||
|
# ---
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from aioshelly.ble.const import BLE_SCRIPT_NAME
|
||||||
from aioshelly.const import MODEL_I3
|
from aioshelly.const import MODEL_I3
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_unordered import unordered
|
from pytest_unordered import unordered
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.event import (
|
from homeassistant.components.event import (
|
||||||
ATTR_EVENT_TYPE,
|
ATTR_EVENT_TYPE,
|
||||||
@ -64,6 +66,99 @@ async def test_rpc_button(
|
|||||||
assert state.attributes.get(ATTR_EVENT_TYPE) == "single_push"
|
assert state.attributes.get(ATTR_EVENT_TYPE) == "single_push"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_rpc_script_1_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
entity_registry: EntityRegistry,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test script event."""
|
||||||
|
await init_integration(hass, 2)
|
||||||
|
entity_id = "event.test_name_test_script_js"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state == snapshot(name=f"{entity_id}-state")
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry == snapshot(name=f"{entity_id}-entry")
|
||||||
|
|
||||||
|
inject_rpc_device_event(
|
||||||
|
monkeypatch,
|
||||||
|
mock_rpc_device,
|
||||||
|
{
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"component": "script:1",
|
||||||
|
"id": 1,
|
||||||
|
"event": "script_start",
|
||||||
|
"ts": 1668522399.2,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ts": 1668522399.2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes.get(ATTR_EVENT_TYPE) == "script_start"
|
||||||
|
|
||||||
|
inject_rpc_device_event(
|
||||||
|
monkeypatch,
|
||||||
|
mock_rpc_device,
|
||||||
|
{
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"component": "script:1",
|
||||||
|
"id": 1,
|
||||||
|
"event": "unknown_event",
|
||||||
|
"ts": 1668522399.2,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ts": 1668522399.2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.attributes.get(ATTR_EVENT_TYPE) != "unknown_event"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_rpc_script_2_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that scripts without any emitEvent will not get an event entity."""
|
||||||
|
await init_integration(hass, 2)
|
||||||
|
entity_id = "event.test_name_test_script_2_js"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state == snapshot(name=f"{entity_id}-state")
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry == snapshot(name=f"{entity_id}-entry")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_rpc_script_ble_event(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test that the ble script will not get an event entity."""
|
||||||
|
await init_integration(hass, 2)
|
||||||
|
entity_id = f"event.test_name_{BLE_SCRIPT_NAME}"
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state == snapshot(name=f"{entity_id}-state")
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry == snapshot(name=f"{entity_id}-entry")
|
||||||
|
|
||||||
|
|
||||||
async def test_rpc_event_removal(
|
async def test_rpc_event_removal(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_rpc_device: Mock,
|
mock_rpc_device: Mock,
|
||||||
|
@ -8,7 +8,7 @@ import pytest
|
|||||||
from homeassistant.components.vodafone_station import DOMAIN
|
from homeassistant.components.vodafone_station import DOMAIN
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
from .const import DEVICE_1_MAC
|
from .const import DEVICE_1_HOST, DEVICE_1_MAC, DEVICE_2_MAC
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
AsyncMock,
|
AsyncMock,
|
||||||
@ -48,11 +48,20 @@ def mock_vodafone_station_router() -> Generator[AsyncMock]:
|
|||||||
connected=True,
|
connected=True,
|
||||||
connection_type="wifi",
|
connection_type="wifi",
|
||||||
ip_address="192.168.1.10",
|
ip_address="192.168.1.10",
|
||||||
name="WifiDevice0",
|
name=DEVICE_1_HOST,
|
||||||
mac=DEVICE_1_MAC,
|
mac=DEVICE_1_MAC,
|
||||||
type="laptop",
|
type="laptop",
|
||||||
wifi="2.4G",
|
wifi="2.4G",
|
||||||
),
|
),
|
||||||
|
DEVICE_2_MAC: VodafoneStationDevice(
|
||||||
|
connected=False,
|
||||||
|
connection_type="lan",
|
||||||
|
ip_address="192.168.1.11",
|
||||||
|
name="LanDevice1",
|
||||||
|
mac=DEVICE_2_MAC,
|
||||||
|
type="desktop",
|
||||||
|
wifi="",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
router.get_sensor_data.return_value = load_json_object_fixture(
|
router.get_sensor_data.return_value = load_json_object_fixture(
|
||||||
"get_sensor_data.json", DOMAIN
|
"get_sensor_data.json", DOMAIN
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
"""Common stuff for Vodafone Station tests."""
|
"""Common stuff for Vodafone Station tests."""
|
||||||
|
|
||||||
|
DEVICE_1_HOST = "WifiDevice0"
|
||||||
DEVICE_1_MAC = "xx:xx:xx:xx:xx:xx"
|
DEVICE_1_MAC = "xx:xx:xx:xx:xx:xx"
|
||||||
|
DEVICE_2_HOST = "LanDevice1"
|
||||||
|
DEVICE_2_MAC = "yy:yy:yy:yy:yy:yy"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_all_entities[device_tracker.vodafone_station_xx_xx_xx_xx_xx_xx-entry]
|
# name: test_all_entities[device_tracker.landevice1-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
}),
|
}),
|
||||||
@ -11,8 +11,8 @@
|
|||||||
'disabled_by': None,
|
'disabled_by': None,
|
||||||
'domain': 'device_tracker',
|
'domain': 'device_tracker',
|
||||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
'entity_id': 'device_tracker.vodafone_station_xx_xx_xx_xx_xx_xx',
|
'entity_id': 'device_tracker.landevice1',
|
||||||
'has_entity_name': False,
|
'has_entity_name': True,
|
||||||
'hidden_by': None,
|
'hidden_by': None,
|
||||||
'icon': None,
|
'icon': None,
|
||||||
'id': <ANY>,
|
'id': <ANY>,
|
||||||
@ -23,7 +23,57 @@
|
|||||||
}),
|
}),
|
||||||
'original_device_class': None,
|
'original_device_class': None,
|
||||||
'original_icon': None,
|
'original_icon': None,
|
||||||
'original_name': None,
|
'original_name': 'LanDevice1',
|
||||||
|
'platform': 'vodafone_station',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'device_tracker',
|
||||||
|
'unique_id': 'yy:yy:yy:yy:yy:yy',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[device_tracker.landevice1-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'LanDevice1',
|
||||||
|
'host_name': 'LanDevice1',
|
||||||
|
'ip': '192.168.1.11',
|
||||||
|
'mac': 'yy:yy:yy:yy:yy:yy',
|
||||||
|
'source_type': <SourceType.ROUTER: 'router'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'device_tracker.landevice1',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'not_home',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities[device_tracker.wifidevice0-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'device_tracker',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'device_tracker.wifidevice0',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'WifiDevice0',
|
||||||
'platform': 'vodafone_station',
|
'platform': 'vodafone_station',
|
||||||
'previous_unique_id': None,
|
'previous_unique_id': None,
|
||||||
'supported_features': 0,
|
'supported_features': 0,
|
||||||
@ -32,16 +82,17 @@
|
|||||||
'unit_of_measurement': None,
|
'unit_of_measurement': None,
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
# name: test_all_entities[device_tracker.vodafone_station_xx_xx_xx_xx_xx_xx-state]
|
# name: test_all_entities[device_tracker.wifidevice0-state]
|
||||||
StateSnapshot({
|
StateSnapshot({
|
||||||
'attributes': ReadOnlyDict({
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'WifiDevice0',
|
||||||
'host_name': 'WifiDevice0',
|
'host_name': 'WifiDevice0',
|
||||||
'ip': '192.168.1.10',
|
'ip': '192.168.1.10',
|
||||||
'mac': 'xx:xx:xx:xx:xx:xx',
|
'mac': 'xx:xx:xx:xx:xx:xx',
|
||||||
'source_type': <SourceType.ROUTER: 'router'>,
|
'source_type': <SourceType.ROUTER: 'router'>,
|
||||||
}),
|
}),
|
||||||
'context': <ANY>,
|
'context': <ANY>,
|
||||||
'entity_id': 'device_tracker.vodafone_station_xx_xx_xx_xx_xx_xx',
|
'entity_id': 'device_tracker.wifidevice0',
|
||||||
'last_changed': <ANY>,
|
'last_changed': <ANY>,
|
||||||
'last_reported': <ANY>,
|
'last_reported': <ANY>,
|
||||||
'last_updated': <ANY>,
|
'last_updated': <ANY>,
|
||||||
|
@ -9,6 +9,12 @@
|
|||||||
'hostname': 'WifiDevice0',
|
'hostname': 'WifiDevice0',
|
||||||
'type': 'laptop',
|
'type': 'laptop',
|
||||||
}),
|
}),
|
||||||
|
dict({
|
||||||
|
'connected': False,
|
||||||
|
'connection_type': 'lan',
|
||||||
|
'hostname': 'LanDevice1',
|
||||||
|
'type': 'desktop',
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
'last_exception': None,
|
'last_exception': None,
|
||||||
'last_update success': True,
|
'last_update success': True,
|
||||||
|
68
tests/components/vodafone_station/test_coordinator.py
Normal file
68
tests/components/vodafone_station/test_coordinator.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
"""Define tests for the Vodafone Station coordinator."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from aiovodafone import VodafoneStationDevice
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.vodafone_station.const import DOMAIN, SCAN_INTERVAL
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
from .const import DEVICE_1_HOST, DEVICE_1_MAC, DEVICE_2_HOST, DEVICE_2_MAC
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_coordinator_device_cleanup(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
mock_vodafone_station_router: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test Device cleanup on coordinator update."""
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
device = device_registry.async_get_or_create(
|
||||||
|
config_entry_id=mock_config_entry.entry_id,
|
||||||
|
identifiers={(DOMAIN, DEVICE_1_MAC)},
|
||||||
|
name=DEVICE_1_HOST,
|
||||||
|
)
|
||||||
|
assert device is not None
|
||||||
|
|
||||||
|
device_tracker = f"device_tracker.{DEVICE_1_HOST}"
|
||||||
|
|
||||||
|
state = hass.states.get(device_tracker)
|
||||||
|
assert state is not None
|
||||||
|
|
||||||
|
mock_vodafone_station_router.get_devices_data.return_value = {
|
||||||
|
DEVICE_2_MAC: VodafoneStationDevice(
|
||||||
|
connected=True,
|
||||||
|
connection_type="lan",
|
||||||
|
ip_address="192.168.1.11",
|
||||||
|
name=DEVICE_2_HOST,
|
||||||
|
mac=DEVICE_2_MAC,
|
||||||
|
type="desktop",
|
||||||
|
wifi="",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
freezer.tick(SCAN_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
|
||||||
|
state = hass.states.get(device_tracker)
|
||||||
|
assert state is None
|
||||||
|
assert f"Skipping entity {DEVICE_2_HOST}" in caplog.text
|
||||||
|
|
||||||
|
device = device_registry.async_get_device(identifiers={(DOMAIN, DEVICE_1_MAC)})
|
||||||
|
assert device is None
|
||||||
|
assert f"Removing device: {DEVICE_1_HOST}" in caplog.text
|
@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from . import setup_integration
|
from . import setup_integration
|
||||||
from .const import DEVICE_1_MAC
|
from .const import DEVICE_1_HOST, DEVICE_1_MAC
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ async def test_consider_home(
|
|||||||
"""Test if device is considered not_home when disconnected."""
|
"""Test if device is considered not_home when disconnected."""
|
||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
device_tracker = "device_tracker.vodafone_station_xx_xx_xx_xx_xx_xx"
|
device_tracker = f"device_tracker.{DEVICE_1_HOST}"
|
||||||
|
|
||||||
state = hass.states.get(device_tracker)
|
state = hass.states.get(device_tracker)
|
||||||
assert state
|
assert state
|
||||||
|
Loading…
x
Reference in New Issue
Block a user