mirror of
https://github.com/home-assistant/core.git
synced 2025-12-12 02:48:19 +00:00
Compare commits
13 Commits
strings/ma
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a676b5812 | ||
|
|
1f4cf67daa | ||
|
|
bb4ec229ce | ||
|
|
ff62b460d5 | ||
|
|
9b48e92940 | ||
|
|
c03b9d1f87 | ||
|
|
3f30df203c | ||
|
|
7fe0d96c88 | ||
|
|
cdc2192bba | ||
|
|
74b1c1f6fd | ||
|
|
69c7a7b0ab | ||
|
|
ef302215cc | ||
|
|
6378f5f02a |
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.3"]
|
||||
"requirements": ["aioasuswrt==1.5.2", "asusrouter==1.21.3"]
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"media_player",
|
||||
"switch",
|
||||
"text",
|
||||
"vacuum",
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==3.1.3"],
|
||||
"requirements": ["python-bsblan==3.1.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This device is already configured.",
|
||||
"reauth_successful": "Reauthentication successful."
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"add_sensor_mapping_hint": "You can now add mappings from any sensor in Home Assistant to {integration_name} using the '+ add sensor mapping' button."
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
|
||||
from . import oauth2
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator, HomeLinkData
|
||||
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.EVENT]
|
||||
|
||||
@@ -44,9 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = HomeLinkData(
|
||||
provider=provider, coordinator=coordinator, last_update_id=None
|
||||
)
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
@@ -54,5 +52,5 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.coordinator.async_on_unload(None)
|
||||
await entry.runtime_data.async_on_unload(None)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TypedDict
|
||||
@@ -17,19 +16,10 @@ from homeassistant.util.ssl import get_default_context
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeLinkConfigEntry = ConfigEntry[HomeLinkData]
|
||||
type HomeLinkConfigEntry = ConfigEntry[HomeLinkCoordinator]
|
||||
type EventCallback = Callable[[HomeLinkEventData], None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class HomeLinkData:
|
||||
"""Class for HomeLink integration runtime data."""
|
||||
|
||||
provider: MQTTProvider
|
||||
coordinator: HomeLinkCoordinator
|
||||
last_update_id: str | None
|
||||
|
||||
|
||||
class HomeLinkEventData(TypedDict):
|
||||
"""Data for a single event."""
|
||||
|
||||
@@ -68,7 +58,7 @@ class HomeLinkCoordinator:
|
||||
self._listeners[target_event_id] = update_callback
|
||||
return partial(self.__async_remove_listener_internal, target_event_id)
|
||||
|
||||
def __async_remove_listener_internal(self, listener_id: str):
|
||||
def __async_remove_listener_internal(self, listener_id: str) -> None:
|
||||
del self._listeners[listener_id]
|
||||
|
||||
@callback
|
||||
@@ -92,7 +82,7 @@ class HomeLinkCoordinator:
|
||||
await self.discover_devices()
|
||||
self.provider.listen(self.on_message)
|
||||
|
||||
async def discover_devices(self):
|
||||
async def discover_devices(self) -> None:
|
||||
"""Discover devices and build the Entities."""
|
||||
self.device_data = await self.provider.discover()
|
||||
|
||||
|
||||
@@ -3,25 +3,24 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.event import EventDeviceClass, EventEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, EVENT_PRESSED
|
||||
from .coordinator import HomeLinkCoordinator, HomeLinkEventData
|
||||
from .coordinator import HomeLinkConfigEntry, HomeLinkCoordinator, HomeLinkEventData
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: HomeLinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the entities for the binary sensor."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
"""Add the entities for the event platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HomeLinkEventEntity(button.id, button.name, device.id, device.name, coordinator)
|
||||
HomeLinkEventEntity(coordinator, button.id, button.name, device.id, device.name)
|
||||
for device in coordinator.device_data
|
||||
for button in device.buttons
|
||||
)
|
||||
@@ -40,11 +39,11 @@ class HomeLinkEventEntity(EventEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeLinkCoordinator,
|
||||
button_id: str,
|
||||
param_name: str,
|
||||
device_id: str,
|
||||
device_name: str,
|
||||
coordinator: HomeLinkCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the event entity."""
|
||||
|
||||
@@ -74,5 +73,4 @@ class HomeLinkEventEntity(EventEntity):
|
||||
if update_data["requestId"] != self.last_request_id:
|
||||
self._trigger_event(EVENT_PRESSED)
|
||||
self.last_request_id = update_data["requestId"]
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"ws_path": "WebSocket path"
|
||||
},
|
||||
"data_description": {
|
||||
"advanced_options": "Enable and select **Next** to set advanced options.",
|
||||
"advanced_options": "Enable and select **Submit** to set advanced options.",
|
||||
"broker": "The hostname or IP address of your MQTT broker.",
|
||||
"certificate": "The custom CA certificate file to validate your MQTT brokers certificate.",
|
||||
"client_cert": "The client certificate to authenticate against your MQTT broker.",
|
||||
|
||||
@@ -56,4 +56,5 @@ WIDGET_TO_WATER_HEATER_ENTITY = {
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = {
|
||||
"modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent,
|
||||
"io:AtlanticDomesticHotWaterProductionV2_CV4E_IOComponent": AtlanticDomesticHotWaterProductionV2IOComponent,
|
||||
"io:AtlanticDomesticHotWaterProductionV2_CETHI_V4_IOComponent": AtlanticDomesticHotWaterProductionV2IOComponent,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smarttub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["smarttub"],
|
||||
"requirements": ["python-smarttub==0.0.45"]
|
||||
"requirements": ["python-smarttub==0.0.46"]
|
||||
}
|
||||
|
||||
@@ -29,5 +29,13 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:toggle-switch-variant"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:toggle-switch-variant-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:toggle-switch-variant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted switches to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"toggle": "[%key:common::device_automation::action_type::toggle%]",
|
||||
@@ -41,6 +45,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"toggle": {
|
||||
"description": "Toggles a switch on/off.",
|
||||
@@ -55,5 +68,27 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Switch"
|
||||
"title": "Switch",
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"description": "Triggers after one or more switches turn off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::switch::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::switch::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch turned off"
|
||||
},
|
||||
"turned_on": {
|
||||
"description": "Triggers after one or more switches turn on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::switch::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::switch::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Switch turned on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
homeassistant/components/switch/trigger.py
Normal file
17
homeassistant/components/switch/trigger.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Provides triggers for switch platform."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for switch platform."""
|
||||
return TRIGGERS
|
||||
18
homeassistant/components/switch/triggers.yaml
Normal file
18
homeassistant/components/switch/triggers.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: switch
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
@@ -380,7 +380,7 @@ class _CustomDPCodeWrapper(DPCodeWrapper):
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||
if (raw_value := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
return raw_value in self._valid_values
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class _DPCodePercentageMappingWrapper(DPCodeIntegerWrapper):
|
||||
return False
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
if (value := self._read_device_status_raw(device)) is None:
|
||||
if (value := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
|
||||
return round(
|
||||
|
||||
@@ -77,7 +77,7 @@ class _AlarmMessageWrapper(DPCodeStringWrapper, _DPCodeEventWrapper):
|
||||
|
||||
def get_event_attributes(self, device: CustomerDevice) -> dict[str, Any] | None:
|
||||
"""Return the event attributes for the alarm message."""
|
||||
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||
if (raw_value := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
return {"message": b64decode(raw_value).decode("utf-8")}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class _BrightnessWrapper(DPCodeIntegerWrapper):
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> Any | None:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
if (brightness := self._read_device_status_raw(device)) is None:
|
||||
if (brightness := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
|
||||
# Remap value to our scale
|
||||
@@ -114,7 +114,7 @@ class _ColorTempWrapper(DPCodeIntegerWrapper):
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> Any | None:
|
||||
"""Return the color temperature value in Kelvin."""
|
||||
if (temperature := self._read_device_status_raw(device)) is None:
|
||||
if (temperature := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
|
||||
return color_util.color_temperature_mired_to_kelvin(
|
||||
|
||||
@@ -46,13 +46,6 @@ class DPCodeWrapper(DeviceWrapper):
|
||||
"""Init DPCodeWrapper."""
|
||||
self.dpcode = dpcode
|
||||
|
||||
def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the raw device status for the DPCode.
|
||||
|
||||
Private helper method for `read_device_status`.
|
||||
"""
|
||||
return device.status.get(self.dpcode)
|
||||
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
||||
"""Convert a Home Assistant value back to a raw device value.
|
||||
|
||||
@@ -90,7 +83,7 @@ class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
|
||||
def read_device_status(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
return self.type_information.process_raw_value(
|
||||
self._read_device_status_raw(device), device
|
||||
device.status.get(self.dpcode), device
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -197,7 +190,7 @@ class DPCodeBitmapBitWrapper(DPCodeWrapper):
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||
if (raw_value := device.status.get(self.dpcode)) is None:
|
||||
return None
|
||||
return (raw_value & (1 << self._mask)) != 0
|
||||
|
||||
|
||||
@@ -76,9 +76,7 @@ class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (
|
||||
raw_value := self._read_device_status_raw(device)
|
||||
) in self.type_information.range:
|
||||
if (raw_value := device.status.get(self.dpcode)) in self.type_information.range:
|
||||
return self._WIND_DIRECTIONS.get(raw_value)
|
||||
return None
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_VLP_FILE, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -98,8 +98,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: VelbusConfigEntry) -> bool:
|
||||
"""Establish connection with velbus."""
|
||||
controller = Velbus(
|
||||
entry.data[CONF_PORT],
|
||||
dsn=entry.data[CONF_PORT],
|
||||
cache_dir=hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"),
|
||||
vlp_file=entry.data.get(CONF_VLP_FILE),
|
||||
)
|
||||
try:
|
||||
await controller.connect()
|
||||
|
||||
@@ -2,18 +2,35 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from typing import Any, Final
|
||||
|
||||
import serial.tools.list_ports
|
||||
import velbusaio.controller
|
||||
from velbusaio.exceptions import VelbusConnectionFailed
|
||||
from velbusaio.vlp_reader import VlpFile
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.components.file_upload import process_uploaded_file
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
|
||||
from .const import CONF_TLS, DOMAIN
|
||||
from .const import CONF_TLS, CONF_VLP_FILE, DOMAIN
|
||||
|
||||
STORAGE_PATH: Final = ".storage/velbus.{key}.vlp"
|
||||
|
||||
|
||||
class InvalidVlpFile(HomeAssistantError):
|
||||
"""Error to indicate that the uploaded file is not a valid VLP file."""
|
||||
|
||||
|
||||
class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -24,14 +41,15 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the velbus config flow."""
|
||||
self._errors: dict[str, str] = {}
|
||||
self._device: str = ""
|
||||
self._vlp_file: str | None = None
|
||||
self._title: str = ""
|
||||
|
||||
def _create_device(self) -> ConfigFlowResult:
|
||||
"""Create an entry async."""
|
||||
return self.async_create_entry(
|
||||
title=self._title, data={CONF_PORT: self._device}
|
||||
title=self._title,
|
||||
data={CONF_PORT: self._device, CONF_VLP_FILE: self._vlp_file},
|
||||
)
|
||||
|
||||
async def _test_connection(self) -> bool:
|
||||
@@ -41,7 +59,6 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await controller.connect()
|
||||
await controller.stop()
|
||||
except VelbusConnectionFailed:
|
||||
self._errors[CONF_PORT] = "cannot_connect"
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -57,6 +74,7 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle network step."""
|
||||
step_errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._title = "Velbus Network"
|
||||
if user_input[CONF_TLS]:
|
||||
@@ -68,7 +86,8 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._device += f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}"
|
||||
self._async_abort_entries_match({CONF_PORT: self._device})
|
||||
if await self._test_connection():
|
||||
return self._create_device()
|
||||
return await self.async_step_vlp()
|
||||
step_errors[CONF_HOST] = "cannot_connect"
|
||||
else:
|
||||
user_input = {
|
||||
CONF_TLS: True,
|
||||
@@ -88,13 +107,14 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
suggested_values=user_input,
|
||||
),
|
||||
errors=self._errors,
|
||||
errors=step_errors,
|
||||
)
|
||||
|
||||
async def async_step_usbselect(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle usb select step."""
|
||||
step_errors: dict[str, str] = {}
|
||||
ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports)
|
||||
list_of_ports = [
|
||||
f"{p}{', s/n: ' + p.serial_number if p.serial_number else ''}"
|
||||
@@ -107,7 +127,8 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._device = ports[list_of_ports.index(user_input[CONF_PORT])].device
|
||||
self._async_abort_entries_match({CONF_PORT: self._device})
|
||||
if await self._test_connection():
|
||||
return self._create_device()
|
||||
return await self.async_step_vlp()
|
||||
step_errors[CONF_PORT] = "cannot_connect"
|
||||
else:
|
||||
user_input = {}
|
||||
user_input[CONF_PORT] = ""
|
||||
@@ -118,7 +139,7 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Schema({vol.Required(CONF_PORT): vol.In(list_of_ports)}),
|
||||
suggested_values=user_input,
|
||||
),
|
||||
errors=self._errors,
|
||||
errors=step_errors,
|
||||
)
|
||||
|
||||
async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult:
|
||||
@@ -144,3 +165,75 @@ class VelbusConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="discovery_confirm",
|
||||
description_placeholders={CONF_NAME: self._title},
|
||||
)
|
||||
|
||||
async def _validate_vlp_file(self, file_path: str) -> None:
|
||||
"""Validate VLP file and raise exception if invalid."""
|
||||
vlpfile = VlpFile(file_path)
|
||||
await vlpfile.read()
|
||||
if not vlpfile.get():
|
||||
raise InvalidVlpFile("no_modules")
|
||||
|
||||
async def async_step_vlp(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Step when user wants to use the VLP file."""
|
||||
step_errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if CONF_VLP_FILE not in user_input or user_input[CONF_VLP_FILE] == "":
|
||||
# The VLP file is optional, so allow skipping it
|
||||
self._vlp_file = None
|
||||
else:
|
||||
try:
|
||||
# handle the file upload
|
||||
self._vlp_file = await self.hass.async_add_executor_job(
|
||||
save_uploaded_vlp_file, self.hass, user_input[CONF_VLP_FILE]
|
||||
)
|
||||
# validate it
|
||||
await self._validate_vlp_file(self._vlp_file)
|
||||
except InvalidVlpFile as e:
|
||||
step_errors[CONF_VLP_FILE] = str(e)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
old_entry = self._get_reconfigure_entry()
|
||||
return self.async_update_reload_and_abort(
|
||||
old_entry,
|
||||
data={
|
||||
CONF_VLP_FILE: self._vlp_file,
|
||||
CONF_PORT: old_entry.data.get(CONF_PORT),
|
||||
},
|
||||
)
|
||||
if not step_errors:
|
||||
return self._create_device()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="vlp",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_VLP_FILE): selector.FileSelector(
|
||||
config=selector.FileSelectorConfig(accept=".vlp")
|
||||
),
|
||||
}
|
||||
),
|
||||
suggested_values=user_input,
|
||||
),
|
||||
errors=step_errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration."""
|
||||
return await self.async_step_vlp()
|
||||
|
||||
|
||||
def save_uploaded_vlp_file(hass: HomeAssistant, uploaded_file_id: str) -> str:
|
||||
"""Validate the uploaded file and move it to the storage directory.
|
||||
|
||||
Blocking function needs to be called in executor.
|
||||
"""
|
||||
|
||||
with process_uploaded_file(hass, uploaded_file_id) as file:
|
||||
dest_path = Path(hass.config.path(STORAGE_PATH.format(key=uploaded_file_id)))
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(file, dest_path)
|
||||
return str(dest_path)
|
||||
|
||||
@@ -14,6 +14,7 @@ DOMAIN: Final = "velbus"
|
||||
CONF_CONFIG_ENTRY: Final = "config_entry"
|
||||
CONF_MEMO_TEXT: Final = "memo_text"
|
||||
CONF_TLS: Final = "tls"
|
||||
CONF_VLP_FILE: Final = "vlp_file"
|
||||
|
||||
SERVICE_SCAN: Final = "scan"
|
||||
SERVICE_SYNC: Final = "sync_clock"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Velbus",
|
||||
"codeowners": ["@Cereal2nd", "@brefra"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"dependencies": ["usb", "file_upload"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/velbus",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_modules": "No Velbus modules found, please check your VLP file."
|
||||
},
|
||||
"step": {
|
||||
"network": {
|
||||
@@ -41,6 +43,16 @@
|
||||
"usbselect": "Via USB device"
|
||||
},
|
||||
"title": "Define the Velbus connection"
|
||||
},
|
||||
"vlp": {
|
||||
"data": {
|
||||
"vlp_file": "Path to VLP file"
|
||||
},
|
||||
"data_description": {
|
||||
"vlp_file": "Select the VLP file from your filesystem."
|
||||
},
|
||||
"description": "You can optionally provide a VLP file to improve module detection. The VLP file is the config file from VelbusLink that contains all module information. If you do not provide it now, you can always add it later in the integration options. Without this file, Home Assistant will try to detect the modules automatically, but this can take longer time and some modules might not be detected correctly.",
|
||||
"title": "Optional VLP file"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -223,3 +223,6 @@ gql<4.0.0
|
||||
|
||||
# Pin pytest-rerunfailures to prevent accidental breaks
|
||||
pytest-rerunfailures==16.0.1
|
||||
|
||||
# pycares 5.x is not yet compatible with aiodns
|
||||
pycares==4.11.0
|
||||
|
||||
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@@ -206,7 +206,7 @@ aioaquacell==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.5.1
|
||||
aioasuswrt==1.5.2
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.1
|
||||
@@ -2475,7 +2475,7 @@ python-awair==0.2.5
|
||||
python-blockchain-api==0.0.2
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==3.1.3
|
||||
python-bsblan==3.1.4
|
||||
|
||||
# homeassistant.components.citybikes
|
||||
python-citybikes==0.3.3
|
||||
@@ -2578,7 +2578,7 @@ python-ripple-api==0.0.3
|
||||
python-roborock==3.12.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.45
|
||||
python-smarttub==0.0.46
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.8.3
|
||||
|
||||
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@@ -197,7 +197,7 @@ aioaquacell==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.5.1
|
||||
aioasuswrt==1.5.2
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.1
|
||||
@@ -2086,7 +2086,7 @@ python-MotionMount==2.3.0
|
||||
python-awair==0.2.5
|
||||
|
||||
# homeassistant.components.bsblan
|
||||
python-bsblan==3.1.3
|
||||
python-bsblan==3.1.4
|
||||
|
||||
# homeassistant.components.ecobee
|
||||
python-ecobee-api==0.3.2
|
||||
@@ -2159,7 +2159,7 @@ python-rabbitair==0.0.8
|
||||
python-roborock==3.12.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.45
|
||||
python-smarttub==0.0.46
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.8.3
|
||||
|
||||
@@ -214,6 +214,9 @@ gql<4.0.0
|
||||
|
||||
# Pin pytest-rerunfailures to prevent accidental breaks
|
||||
pytest-rerunfailures==16.0.1
|
||||
|
||||
# pycares 5.x is not yet compatible with aiodns
|
||||
pycares==4.11.0
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
||||
@@ -1 +1,32 @@
|
||||
"""Tests for the homelink integration."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Set up the homelink integration for testing."""
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def update_callback(
|
||||
hass: HomeAssistant, mock: AsyncMock, update_type: str, data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Invoke the MQTT provider's message callback with the specified update type and data."""
|
||||
for call in mock.listen.call_args_list:
|
||||
call[0][0](
|
||||
"topic",
|
||||
{
|
||||
"type": update_type,
|
||||
"data": data,
|
||||
},
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from homelink.model.button import Button
|
||||
import homelink.model.device
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.gentex_homelink import DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_srp_auth() -> Generator[AsyncMock]:
|
||||
@@ -24,6 +30,50 @@ def mock_srp_auth() -> Generator[AsyncMock]:
|
||||
yield instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_mqtt_provider(mock_device: AsyncMock) -> Generator[AsyncMock]:
|
||||
"""Mock MQTT provider."""
|
||||
with patch(
|
||||
"homeassistant.components.gentex_homelink.MQTTProvider", autospec=True
|
||||
) as mock_mqtt_provider:
|
||||
instance = mock_mqtt_provider.return_value
|
||||
instance.discover.return_value = [mock_device]
|
||||
yield instance
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_device() -> AsyncMock:
|
||||
"""Mock Device instance."""
|
||||
device = AsyncMock(spec=homelink.model.device.Device, autospec=True)
|
||||
buttons = [
|
||||
Button(id="1", name="Button 1", device=device),
|
||||
Button(id="2", name="Button 2", device=device),
|
||||
Button(id="3", name="Button 3", device=device),
|
||||
]
|
||||
device.id = "TestDevice"
|
||||
device.name = "TestDevice"
|
||||
device.buttons = buttons
|
||||
return device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock setup entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
"auth_implementation": "gentex_homelink",
|
||||
"token": {
|
||||
"access_token": "access",
|
||||
"refresh_token": "refresh",
|
||||
"expires_in": 3600,
|
||||
"token_type": "bearer",
|
||||
"expires_at": 1234567890,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Mock setup entry."""
|
||||
|
||||
172
tests/components/gentex_homelink/snapshots/test_event.ambr
Normal file
172
tests/components/gentex_homelink/snapshots/test_event.ambr
Normal file
@@ -0,0 +1,172 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[event.testdevice_button_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.testdevice_button_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Button 1',
|
||||
'platform': 'gentex_homelink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[event.testdevice_button_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
'friendly_name': 'TestDevice Button 1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.testdevice_button_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[event.testdevice_button_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.testdevice_button_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Button 2',
|
||||
'platform': 'gentex_homelink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[event.testdevice_button_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
'friendly_name': 'TestDevice Button 2',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.testdevice_button_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[event.testdevice_button_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'event',
|
||||
'entity_category': None,
|
||||
'entity_id': 'event.testdevice_button_3',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <EventDeviceClass.BUTTON: 'button'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Button 3',
|
||||
'platform': 'gentex_homelink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '3',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[event.testdevice_button_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'button',
|
||||
'event_type': None,
|
||||
'event_types': list([
|
||||
'Pressed',
|
||||
]),
|
||||
'friendly_name': 'TestDevice Button 3',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'event.testdevice_button_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
32
tests/components/gentex_homelink/snapshots/test_init.ambr
Normal file
32
tests/components/gentex_homelink/snapshots/test_init.ambr
Normal file
@@ -0,0 +1,32 @@
|
||||
# serializer version: 1
|
||||
# name: test_device
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'gentex_homelink',
|
||||
'TestDevice',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': None,
|
||||
'model': None,
|
||||
'model_id': None,
|
||||
'name': 'TestDevice',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -1,160 +0,0 @@
|
||||
"""Tests for the homelink coordinator."""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
from homelink.model.button import Button
|
||||
from homelink.model.device import Device
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.gentex_homelink import async_setup_entry
|
||||
from homeassistant.components.gentex_homelink.const import EVENT_PRESSED
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
DOMAIN = "gentex_homelink"
|
||||
|
||||
deviceInst = Device(id="TestDevice", name="TestDevice")
|
||||
deviceInst.buttons = [
|
||||
Button(id="Button 1", name="Button 1", device=deviceInst),
|
||||
Button(id="Button 2", name="Button 2", device=deviceInst),
|
||||
Button(id="Button 3", name="Button 3", device=deviceInst),
|
||||
]
|
||||
|
||||
|
||||
async def test_get_state_updates(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test state updates.
|
||||
|
||||
Tests that get_state calls are called by home assistant, and the homeassistant components respond appropriately to the data returned.
|
||||
"""
|
||||
with patch(
|
||||
"homeassistant.components.gentex_homelink.MQTTProvider", autospec=True
|
||||
) as MockProvider:
|
||||
instance = MockProvider.return_value
|
||||
instance.discover.return_value = [deviceInst]
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=None,
|
||||
version=1,
|
||||
data={
|
||||
"auth_implementation": "gentex_homelink",
|
||||
"token": {"expires_at": time.time() + 10000, "access_token": ""},
|
||||
"last_update_id": None,
|
||||
},
|
||||
state=ConfigEntryState.LOADED,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await async_setup_entry(hass, config_entry)
|
||||
# Assert configuration worked without errors
|
||||
assert result
|
||||
|
||||
provider = config_entry.runtime_data.provider
|
||||
state_data = {
|
||||
"type": "state",
|
||||
"data": {
|
||||
"Button 1": {"requestId": "rid1", "timestamp": time.time()},
|
||||
"Button 2": {"requestId": "rid2", "timestamp": time.time()},
|
||||
"Button 3": {"requestId": "rid3", "timestamp": time.time()},
|
||||
},
|
||||
}
|
||||
|
||||
# Test successful setup and first data fetch. The buttons should be unknown at the start
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
states = hass.states.async_all()
|
||||
assert states, "No states were loaded"
|
||||
assert all(state != STATE_UNAVAILABLE for state in states), (
|
||||
"At least one state was not initialized as STATE_UNAVAILABLE"
|
||||
)
|
||||
buttons_unknown = [s.state == "unknown" for s in states]
|
||||
assert buttons_unknown and all(buttons_unknown), (
|
||||
"At least one button state was not initialized to unknown"
|
||||
)
|
||||
|
||||
provider.listen.mock_calls[0].args[0](None, state_data)
|
||||
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await asyncio.gather(*asyncio.all_tasks() - {asyncio.current_task()})
|
||||
await asyncio.sleep(0.01)
|
||||
states = hass.states.async_all()
|
||||
|
||||
assert all(state != STATE_UNAVAILABLE for state in states), (
|
||||
"Some button became unavailable"
|
||||
)
|
||||
buttons_pressed = [s.attributes["event_type"] == EVENT_PRESSED for s in states]
|
||||
assert buttons_pressed and all(buttons_pressed), (
|
||||
"At least one button was not pressed"
|
||||
)
|
||||
|
||||
|
||||
async def test_request_sync(hass: HomeAssistant) -> None:
|
||||
"""Test that the config entry is reloaded when a requestSync request is sent."""
|
||||
updatedDeviceInst = Device(id="TestDevice", name="TestDevice")
|
||||
updatedDeviceInst.buttons = [
|
||||
Button(id="Button 1", name="New Button 1", device=deviceInst),
|
||||
Button(id="Button 2", name="New Button 2", device=deviceInst),
|
||||
Button(id="Button 3", name="New Button 3", device=deviceInst),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.gentex_homelink.MQTTProvider", autospec=True
|
||||
) as MockProvider:
|
||||
instance = MockProvider.return_value
|
||||
instance.discover.side_effect = [[deviceInst], [updatedDeviceInst]]
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=None,
|
||||
version=1,
|
||||
data={
|
||||
"auth_implementation": "gentex_homelink",
|
||||
"token": {"expires_at": time.time() + 10000, "access_token": ""},
|
||||
"last_update_id": None,
|
||||
},
|
||||
state=ConfigEntryState.LOADED,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await async_setup_entry(hass, config_entry)
|
||||
# Assert configuration worked without errors
|
||||
assert result
|
||||
|
||||
# Check to see if the correct buttons names were loaded
|
||||
comp = er.async_get(hass)
|
||||
button_names = {"Button 1", "Button 2", "Button 3"}
|
||||
registered_button_names = {b.original_name for b in comp.entities.values()}
|
||||
|
||||
assert button_names == registered_button_names, (
|
||||
"Expect button names to be correct for the initial config"
|
||||
)
|
||||
|
||||
provider = config_entry.runtime_data.provider
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
|
||||
with patch.object(
|
||||
coordinator.hass.config_entries, "async_reload"
|
||||
) as async_reload_mock:
|
||||
# Mimic request sync event
|
||||
state_data = {
|
||||
"type": "requestSync",
|
||||
}
|
||||
# async reload should not be called yet
|
||||
async_reload_mock.assert_not_called()
|
||||
# Send the request sync
|
||||
provider.listen.mock_calls[0].args[0](None, state_data)
|
||||
# Wait for the request to be processed
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
await asyncio.gather(*asyncio.all_tasks() - {asyncio.current_task()})
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
# Now async reload should have been called
|
||||
async_reload_mock.assert_called()
|
||||
@@ -1,77 +1,55 @@
|
||||
"""Test that the devices and entities are correctly configured."""
|
||||
|
||||
from unittest.mock import patch
|
||||
import time
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homelink.model.button import Button
|
||||
from homelink.model.device import Device
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.gentex_homelink import async_setup_entry
|
||||
from homeassistant.components.gentex_homelink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import ClientSessionGenerator
|
||||
from . import setup_integration, update_callback
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
TEST_CONFIG_ENTRY_ID = "ABC123"
|
||||
|
||||
"""Mock classes for testing."""
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
deviceInst = Device(id="TestDevice", name="TestDevice")
|
||||
deviceInst.buttons = [
|
||||
Button(id="1", name="Button 1", device=deviceInst),
|
||||
Button(id="2", name="Button 2", device=deviceInst),
|
||||
Button(id="3", name="Button 3", device=deviceInst),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_setup_config(
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mqtt_provider: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Setup config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.gentex_homelink.MQTTProvider", autospec=True
|
||||
) as MockProvider:
|
||||
instance = MockProvider.return_value
|
||||
instance.discover.return_value = [deviceInst]
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=None,
|
||||
version=1,
|
||||
data={"auth_implementation": "gentex_homelink"},
|
||||
state=ConfigEntryState.LOADED,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
result = await async_setup_entry(hass, config_entry)
|
||||
|
||||
# Assert configuration worked without errors
|
||||
assert result
|
||||
|
||||
|
||||
async def test_device_registered(hass: HomeAssistant, test_setup_config) -> None:
|
||||
"""Check if a device is registered."""
|
||||
# Assert we got a device with the test ID
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_device([(DOMAIN, "TestDevice")])
|
||||
assert device
|
||||
assert device.name == "TestDevice"
|
||||
|
||||
|
||||
def test_entities_registered(hass: HomeAssistant, test_setup_config) -> None:
|
||||
"""Check if the entities are registered."""
|
||||
comp = er.async_get(hass)
|
||||
button_names = {"Button 1", "Button 2", "Button 3"}
|
||||
registered_button_names = {b.original_name for b in comp.entities.values()}
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert button_names == registered_button_names
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2021-07-30")
|
||||
async def test_entities_update(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mqtt_provider: AsyncMock,
|
||||
) -> None:
|
||||
"""Check if the entities are updated."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get("event.testdevice_button_1").state == STATE_UNKNOWN
|
||||
|
||||
await update_callback(
|
||||
hass,
|
||||
mock_mqtt_provider,
|
||||
"state",
|
||||
{
|
||||
"1": {"requestId": "rid1", "timestamp": time.time()},
|
||||
"2": {"requestId": "rid2", "timestamp": time.time()},
|
||||
"3": {"requestId": "rid3", "timestamp": time.time()},
|
||||
},
|
||||
)
|
||||
assert (
|
||||
hass.states.get("event.testdevice_button_1").state
|
||||
== "2021-07-30T00:00:00.000+00:00"
|
||||
)
|
||||
|
||||
@@ -1,32 +1,66 @@
|
||||
"""Test that the integration is initialized correctly."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import gentex_homelink
|
||||
from homeassistant.components.gentex_homelink.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
from . import setup_integration, update_callback
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mqtt_provider: AsyncMock,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test device is registered correctly."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, "TestDevice")},
|
||||
)
|
||||
assert device
|
||||
assert device == snapshot
|
||||
|
||||
|
||||
async def test_reload_sync(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mqtt_provider: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the config entry is reloaded when a requestSync request is sent."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
with patch.object(hass.config_entries, "async_reload") as async_reload_mock:
|
||||
await update_callback(
|
||||
hass,
|
||||
mock_mqtt_provider,
|
||||
"requestSync",
|
||||
{},
|
||||
)
|
||||
|
||||
async_reload_mock.assert_called_once_with(mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_load_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_mqtt_provider: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the entry can be loaded and unloaded."""
|
||||
with patch("homeassistant.components.gentex_homelink.MQTTProvider", autospec=True):
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=None,
|
||||
version=1,
|
||||
data={"auth_implementation": "gentex_homelink"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {}) is True, (
|
||||
"Component is not set up"
|
||||
)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await gentex_homelink.async_unload_entry(hass, entry), (
|
||||
"Component not unloaded"
|
||||
)
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
228
tests/components/switch/test_trigger.py
Normal file
228
tests/components/switch/test_trigger.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""Test switch triggers."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.switch import DOMAIN
|
||||
from homeassistant.const import ATTR_LABEL_ID, CONF_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
|
||||
from tests.components import (
|
||||
StateDescription,
|
||||
arm_trigger,
|
||||
parametrize_target_entities,
|
||||
parametrize_trigger_states,
|
||||
set_or_remove_state,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
|
||||
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
|
||||
"""Stub copying the blueprints to the config folder."""
|
||||
|
||||
|
||||
@pytest.fixture(name="enable_experimental_triggers_conditions")
|
||||
def enable_experimental_triggers_conditions() -> Generator[None]:
|
||||
"""Enable experimental triggers and conditions."""
|
||||
with patch(
|
||||
"homeassistant.components.labs.async_is_preview_feature_enabled",
|
||||
return_value=True,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_switches(hass: HomeAssistant) -> list[str]:
|
||||
"""Create multiple switch entities associated with different targets."""
|
||||
return (await target_entities(hass, DOMAIN))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"switch.turned_off",
|
||||
"switch.turned_on",
|
||||
],
|
||||
)
|
||||
async def test_switch_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the switch triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_state_trigger_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_switches: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the switch state trigger fires when any switch state changes to a specific state."""
|
||||
other_entity_ids = set(target_switches) - {entity_id}
|
||||
|
||||
# Set all switches, including the tested one, to the initial state
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Check if changing other switches also triggers
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == (entities_in_target - 1) * state["count"]
|
||||
service_calls.clear()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_state_trigger_behavior_first(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_switches: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the switch state trigger fires when the first switch changes to a specific state."""
|
||||
other_entity_ids = set(target_switches) - {entity_id}
|
||||
|
||||
# Set all switches, including the tested one, to the initial state
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "first"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
|
||||
# Triggering other switches should not cause the trigger to fire again
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_experimental_triggers_conditions")
|
||||
@pytest.mark.parametrize(
|
||||
("trigger_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities(DOMAIN),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("trigger", "states"),
|
||||
[
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_off",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
),
|
||||
*parametrize_trigger_states(
|
||||
trigger="switch.turned_on",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_state_trigger_behavior_last(
|
||||
hass: HomeAssistant,
|
||||
service_calls: list[ServiceCall],
|
||||
target_switches: list[str],
|
||||
trigger_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
trigger: str,
|
||||
states: list[StateDescription],
|
||||
) -> None:
|
||||
"""Test that the switch state trigger fires when the last switch changes to a specific state."""
|
||||
other_entity_ids = set(target_switches) - {entity_id}
|
||||
|
||||
# Set all switches, including the tested one, to the initial state
|
||||
for eid in target_switches:
|
||||
set_or_remove_state(hass, eid, states[0]["included"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await arm_trigger(hass, trigger, {"behavior": "last"}, trigger_target_config)
|
||||
|
||||
for state in states[1:]:
|
||||
included_state = state["included"]
|
||||
for other_entity_id in other_entity_ids:
|
||||
set_or_remove_state(hass, other_entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == 0
|
||||
|
||||
set_or_remove_state(hass, entity_id, included_state)
|
||||
await hass.async_block_till_done()
|
||||
assert len(service_calls) == state["count"]
|
||||
for service_call in service_calls:
|
||||
assert service_call.data[CONF_ENTITY_ID] == entity_id
|
||||
service_calls.clear()
|
||||
@@ -43,31 +43,6 @@ async def target_texts(hass: HomeAssistant) -> list[str]:
|
||||
return (await target_entities(hass, "text"))["included"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_key",
|
||||
[
|
||||
"alarm_control_panel.armed",
|
||||
"alarm_control_panel.armed_away",
|
||||
"alarm_control_panel.armed_home",
|
||||
"alarm_control_panel.armed_night",
|
||||
"alarm_control_panel.armed_vacation",
|
||||
"alarm_control_panel.disarmed",
|
||||
"alarm_control_panel.triggered",
|
||||
],
|
||||
)
|
||||
async def test_alarm_control_panel_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
) -> None:
|
||||
"""Test the ACP triggers are gated by the labs flag."""
|
||||
await arm_trigger(hass, trigger_key, None, {ATTR_LABEL_ID: "test_label"})
|
||||
assert (
|
||||
"Unnamed automation failed to setup triggers and has been disabled: Trigger "
|
||||
f"'{trigger_key}' requires the experimental 'New triggers and conditions' "
|
||||
"feature to be enabled in Home Assistant Labs settings (feature flag: "
|
||||
"'new_triggers_conditions')"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trigger_key", ["text.changed"])
|
||||
async def test_text_triggers_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, trigger_key: str
|
||||
|
||||
@@ -333,3 +333,82 @@ async def mock_config_entry(
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_vlp_file")
|
||||
def mock_vlp_content():
|
||||
"""Mock vlp file content."""
|
||||
return b"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<Settings>
|
||||
<Name></Name>
|
||||
<Comments></Comments>
|
||||
<DateModified>21/03/2025 12:30:06</DateModified>
|
||||
<Version>11.6.4.0</Version>
|
||||
</Settings>
|
||||
<Preferences>
|
||||
<MasterClock>
|
||||
<IgnoreMultipleClocks>0</IgnoreMultipleClocks>
|
||||
<IgnoreNoSignumClock>0</IgnoreNoSignumClock>
|
||||
<IgnoreNoMasterClock>0</IgnoreNoMasterClock>
|
||||
</MasterClock>
|
||||
</Preferences>
|
||||
<Customer>
|
||||
<Name></Name>
|
||||
<Address></Address>
|
||||
<City></City>
|
||||
<Zip></Zip>
|
||||
<Phone></Phone>
|
||||
<Fax></Fax>
|
||||
<Email></Email>
|
||||
<Email2></Email2>
|
||||
<Country></Country>
|
||||
</Customer>
|
||||
<Installer>
|
||||
<Name></Name>
|
||||
<Address></Address>
|
||||
<City></City>
|
||||
<Zip></Zip>
|
||||
<Phone></Phone>
|
||||
<Fax></Fax>
|
||||
<Email></Email>
|
||||
<Email2></Email2>
|
||||
<Country></Country>
|
||||
</Installer>
|
||||
<Connection>
|
||||
<NetworkHost>192.168.88.9</NetworkHost>
|
||||
<NetworkPort>27015</NetworkPort>
|
||||
<NetworkHostname>7b95834e</NetworkHostname>
|
||||
<NetworkSsl>True</NetworkSsl>
|
||||
<NetworkPassword></NetworkPassword>
|
||||
<NetworkRequireAuth>False</NetworkRequireAuth>
|
||||
</Connection>
|
||||
<Modules>
|
||||
<Module build="2235" address="FE" type="VMBSIG" serial="34DF" locked="0" layer="0" terminator="0">
|
||||
<Caption>VMBSIG</Caption>
|
||||
<Remark></Remark>
|
||||
<Memory>564D42534947FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0300010001000101FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</Memory>
|
||||
<Snapshot>564D42534947FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF030001000100010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</Snapshot>
|
||||
<SnapshotVerification>FFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000</SnapshotVerification>
|
||||
</Module>
|
||||
<Module build="2423" address="CE" type="VMB4LEDPWM-20" serial="BD30" locked="0" layer="0" terminator="0">
|
||||
<Caption>VMB4LEDPWM-20</Caption>
|
||||
<Remark></Remark>
|
||||
<Memory>4368616E6E656C2031FFFFFFFFFFFFFF4368616E6E656C2032FFFFFFFFFFFFFF4368616E6E656C2033FFFFFFFFFFFFFF4368616E6E656C2034FFFFFFFFFFFFFF000000750700160008001700082AF5ECE3E7DDDFDEE5E8F2FD071115181818171A1A1A150E02FFFF1027171A1C151B181A17171209FEF2EAE2E0DCE1DEE4EAF5010DFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000564D42344C454450574D2D3230FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFF</Memory>
|
||||
<Snapshot>4368616E6E656C2031FFFFFFFFFFFFFF4368616E6E656C2032FFFFFFFFFFFFFF4368616E6E656C2033FFFFFFFFFFFFFF4368616E6E656C2034FFFFFFFFFFFFFF000000750700160008001700082AF5ECE3E7DDDFDEE5E8F2FD071115181818171A1A1A150E02FFFF1027171A1C151B181A17171209FEF2EAE2E0DCE1DEE4EAF5010DFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000564D42344C454450574D2D3230FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFF0601FE873FFFFFFFFF7FFFFFFFFFBFFFFFFFFFFEFFFFFFFFBFFFFFFFFF7FFFFFFFFF3FFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000FFFFFFFFFFFFFFFF</Snapshot>
|
||||
<SnapshotVerification>FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF</SnapshotVerification>
|
||||
</Module>
|
||||
</Modules>
|
||||
<Layers/>
|
||||
<EnergyMonitoring>
|
||||
<Counters/>
|
||||
<Consumers/>
|
||||
<Functions/>
|
||||
<Settings>
|
||||
<AutoMaxConsumption>True</AutoMaxConsumption>
|
||||
<MaxConsumption>0</MaxConsumption>
|
||||
<MinInjection>0</MinInjection>
|
||||
<IdleConsumption>0</IdleConsumption>
|
||||
</Settings>
|
||||
</EnergyMonitoring>
|
||||
</Project>"""
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
"""Tests for the Velbus config flow."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import Generator, Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
import serial.tools.list_ports
|
||||
from velbusaio.exceptions import VelbusConnectionFailed
|
||||
|
||||
from homeassistant.components.velbus.const import CONF_TLS, DOMAIN
|
||||
from homeassistant.components.velbus.const import CONF_TLS, CONF_VLP_FILE, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USB, SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
|
||||
from . import init_integration
|
||||
from .const import PORT_SERIAL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -40,6 +44,36 @@ def com_port():
|
||||
return port
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_process_uploaded_file(
|
||||
tmp_path: Path, mock_vlp_file: str
|
||||
) -> Generator[MagicMock]:
|
||||
"""Mock upload vlp file."""
|
||||
file_id_vlp = str(uuid4())
|
||||
|
||||
@contextmanager
|
||||
def _mock_process_uploaded_file(
|
||||
hass: HomeAssistant, uploaded_file_id: str
|
||||
) -> Iterator[Path | None]:
|
||||
with open(tmp_path / uploaded_file_id, "wb") as vlpfile:
|
||||
vlpfile.write(mock_vlp_file)
|
||||
yield tmp_path / uploaded_file_id
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.velbus.config_flow.process_uploaded_file",
|
||||
side_effect=_mock_process_uploaded_file,
|
||||
) as mock_upload,
|
||||
patch(
|
||||
"shutil.move",
|
||||
),
|
||||
):
|
||||
mock_upload.file_id = {
|
||||
CONF_VLP_FILE: file_id_vlp,
|
||||
}
|
||||
yield mock_upload
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def override_async_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
@@ -109,16 +143,57 @@ async def test_user_network_succes(
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "vlp"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "Velbus Network"
|
||||
data = result.get("data")
|
||||
assert data
|
||||
assert data[CONF_PORT] == expected
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
@pytest.mark.usefixtures("controller_connection_failed")
|
||||
async def test_user_network_connect_failure(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test user network config."""
|
||||
# inttial menu show
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result
|
||||
assert result.get("flow_id")
|
||||
assert result.get("type") is FlowResultType.MENU
|
||||
assert result.get("step_id") == "user"
|
||||
assert result.get("menu_options") == ["network", "usbselect"]
|
||||
# select the network option
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{"next_step_id": "network"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
# fill in the network form
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{
|
||||
CONF_HOST: "velbus",
|
||||
CONF_PORT: 6000,
|
||||
CONF_TLS: True,
|
||||
CONF_PASSWORD: "password",
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("errors") == {"host": "cannot_connect"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller_connection_failed")
|
||||
@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
|
||||
async def test_user_usb_succes(hass: HomeAssistant) -> None:
|
||||
async def test_user_usb_connect_failure(hass: HomeAssistant) -> None:
|
||||
"""Test user usb step."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
@@ -135,6 +210,36 @@ async def test_user_usb_succes(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("errors") == {"port": "cannot_connect"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
@patch("serial.tools.list_ports.comports", MagicMock(return_value=[com_port()]))
|
||||
async def test_user_usb_success(hass: HomeAssistant) -> None:
|
||||
"""Test user usb step."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{"next_step_id": "usbselect"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PORT: USB_DEV,
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "vlp"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "Velbus USB"
|
||||
data = result.get("data")
|
||||
@@ -142,6 +247,142 @@ async def test_user_usb_succes(hass: HomeAssistant) -> None:
|
||||
assert data[CONF_PORT] == PORT_SERIAL
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
async def test_vlp_step_no_modules(
|
||||
hass: HomeAssistant,
|
||||
mock_process_uploaded_file: MagicMock,
|
||||
) -> None:
|
||||
"""Test VLP step."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{"next_step_id": "network"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{
|
||||
CONF_TLS: False,
|
||||
CONF_HOST: "192.168.88.9",
|
||||
CONF_PORT: 27015,
|
||||
CONF_PASSWORD: "",
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "vlp"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.velbus.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.read",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.get",
|
||||
return_value=[],
|
||||
),
|
||||
):
|
||||
file_id = mock_process_uploaded_file.file_id
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{CONF_VLP_FILE: file_id[CONF_VLP_FILE]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("errors") == {CONF_VLP_FILE: "no_modules"}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
async def test_vlp_step_success(
|
||||
hass: HomeAssistant,
|
||||
mock_process_uploaded_file: MagicMock,
|
||||
) -> None:
|
||||
"""Test VLP step."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{"next_step_id": "network"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{
|
||||
CONF_TLS: False,
|
||||
CONF_HOST: "192.168.88.9",
|
||||
CONF_PORT: 27015,
|
||||
CONF_PASSWORD: "",
|
||||
},
|
||||
)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "vlp"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.velbus.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.read",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.get",
|
||||
return_value=[1, 2, 3, 4],
|
||||
),
|
||||
):
|
||||
file_id = mock_process_uploaded_file.file_id
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{CONF_VLP_FILE: file_id[CONF_VLP_FILE]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
async def test_reconfigure_step(
|
||||
hass: HomeAssistant,
|
||||
mock_process_uploaded_file: MagicMock,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Testcase for the reconfigure step."""
|
||||
await init_integration(hass, config_entry)
|
||||
result = await config_entry.start_reconfigure_flow(hass)
|
||||
assert result
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "vlp"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.read",
|
||||
AsyncMock(return_value=True),
|
||||
),
|
||||
patch(
|
||||
"velbusaio.vlp_reader.VlpFile.get",
|
||||
return_value=[1, 2, 3, 4],
|
||||
),
|
||||
):
|
||||
file_id = mock_process_uploaded_file.file_id
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result.get("flow_id"),
|
||||
{CONF_VLP_FILE: file_id[CONF_VLP_FILE]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("controller")
|
||||
async def test_network_abort_if_already_setup(hass: HomeAssistant) -> None:
|
||||
"""Test we abort if Velbus is already setup."""
|
||||
@@ -158,7 +399,7 @@ async def test_network_abort_if_already_setup(hass: HomeAssistant) -> None:
|
||||
{"next_step_id": "network"},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
result.get("flow_id"),
|
||||
{
|
||||
CONF_TLS: False,
|
||||
CONF_HOST: "127.0.0.1",
|
||||
|
||||
Reference in New Issue
Block a user