mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add new integration for Dio Chacon cover devices (#116267)
* Dio Chacon integration addition with config flow and cover entity * Addition of model information for device * Addition of light and service to force reloading states * Logger improvements * Convert light to switch and usage of v1.0.0 of the api * 100% for tests coverage * Invalid credential implementation and rebase on latest ha dev code * Simplify PR with only one platform * Ruff correction * restore original .gitignore content * Correction of cover state bug when using cover when using actions on cover group. * Begin of corrections following review. * unit tests correction * Refactor with a coordinator as asked by review * Implemented a post constructor callback init method via dio-chacon-api-1.0.2. Improved typing. * Corrections for 2nd review * Reimplemented without coordinator as reviewed with Joostlek * Review improvement * generalize callback in entity * Other review improvements * Refactored tests for readability * Test 100% operationals * Tests review corrections * Tests review corrections * Review tests improvements * simplified tests with snapshots and callback method * Final fixes * Final fixes * Final fixes * Rename to chacon_dio --------- Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
parent
28f06cb5a0
commit
092e362f01
@ -237,6 +237,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ccm15/ @ocalvo
|
||||
/homeassistant/components/cert_expiry/ @jjlawren
|
||||
/tests/components/cert_expiry/ @jjlawren
|
||||
/homeassistant/components/chacon_dio/ @cnico
|
||||
/tests/components/chacon_dio/ @cnico
|
||||
/homeassistant/components/cisco_ios/ @fbradyirl
|
||||
/homeassistant/components/cisco_mobility_express/ @fbradyirl
|
||||
/homeassistant/components/cisco_webex_teams/ @fbradyirl
|
||||
|
80
homeassistant/components/chacon_dio/__init__.py
Normal file
80
homeassistant/components/chacon_dio/__init__.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""The chacon_dio integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChaconDioData:
|
||||
"""Chacon Dio data class."""
|
||||
|
||||
client: DIOChaconAPIClient
|
||||
list_devices: list[dict[str, Any]]
|
||||
|
||||
|
||||
type ChaconDioConfigEntry = ConfigEntry[ChaconDioData]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ChaconDioConfigEntry) -> bool:
|
||||
"""Set up chacon_dio from a config entry."""
|
||||
|
||||
config = entry.data
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
dio_chacon_id = entry.unique_id
|
||||
|
||||
_LOGGER.debug("Initializing Chacon Dio client %s, %s", username, dio_chacon_id)
|
||||
client = DIOChaconAPIClient(
|
||||
username,
|
||||
password,
|
||||
dio_chacon_id,
|
||||
)
|
||||
|
||||
found_devices = await client.search_all_devices(with_state=True)
|
||||
list_devices = list(found_devices.values())
|
||||
_LOGGER.debug("List of devices %s", list_devices)
|
||||
|
||||
entry.runtime_data = ChaconDioData(
|
||||
client=client,
|
||||
list_devices=list_devices,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Disconnect the permanent websocket connection of home assistant on shutdown
|
||||
async def _async_disconnect_websocket(_: Event) -> None:
|
||||
await client.disconnect()
|
||||
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
await entry.runtime_data.client.disconnect()
|
||||
|
||||
return unload_ok
|
67
homeassistant/components/chacon_dio/config_flow.py
Normal file
67
homeassistant/components/chacon_dio/config_flow.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Config flow for chacon_dio integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for chacon_dio."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
client = DIOChaconAPIClient(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
try:
|
||||
_user_id: str = await client.get_user_id()
|
||||
except DIOChaconAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except DIOChaconInvalidAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
else:
|
||||
await self.async_set_unique_id(_user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=f"Chacon DiO {user_input[CONF_USERNAME]}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
5
homeassistant/components/chacon_dio/const.py
Normal file
5
homeassistant/components/chacon_dio/const.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Constants for the chacon_dio integration."""
|
||||
|
||||
DOMAIN = "chacon_dio"
|
||||
|
||||
MANUFACTURER = "Chacon"
|
124
homeassistant/components/chacon_dio/cover.py
Normal file
124
homeassistant/components/chacon_dio/cover.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""Cover Platform for Chacon Dio REV-SHUTTER devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import ChaconDioConfigEntry
|
||||
from .entity import ChaconDioEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ChaconDioConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Chacon Dio cover devices."""
|
||||
data = config_entry.runtime_data
|
||||
client = data.client
|
||||
|
||||
async_add_entities(
|
||||
ChaconDioCover(client, device)
|
||||
for device in data.list_devices
|
||||
if device["type"] == DeviceTypeEnum.SHUTTER.value
|
||||
)
|
||||
|
||||
|
||||
class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
"""Object for controlling a Chacon Dio cover."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.SHUTTER
|
||||
_attr_name = None
|
||||
|
||||
_attr_supported_features = (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recomputes the attributes values either at init or when the device state changes."""
|
||||
self._attr_available = data["connected"]
|
||||
self._attr_current_cover_position = data["openlevel"]
|
||||
self._attr_is_closing = data["movement"] == ShutterMoveEnum.DOWN.value
|
||||
self._attr_is_opening = data["movement"] == ShutterMoveEnum.UP.value
|
||||
self._attr_is_closed = self._attr_current_cover_position == 0
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover.
|
||||
|
||||
Closed status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Close cover %s , %s, %s",
|
||||
self.target_id,
|
||||
self._attr_name,
|
||||
self.is_closed,
|
||||
)
|
||||
|
||||
# closes effectively only if cover is not already closing and not fully closed
|
||||
if not self._attr_is_closing and not self.is_closed:
|
||||
self._attr_is_closing = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(
|
||||
self.target_id, ShutterMoveEnum.DOWN
|
||||
)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover.
|
||||
|
||||
Opened status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Open cover %s , %s, %s",
|
||||
self.target_id,
|
||||
self._attr_name,
|
||||
self.current_cover_position,
|
||||
)
|
||||
|
||||
# opens effectively only if cover is not already opening and not fully opened
|
||||
if not self._attr_is_opening and self.current_cover_position != 100:
|
||||
self._attr_is_opening = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.UP)
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
|
||||
_LOGGER.debug("Stop cover %s , %s", self.target_id, self._attr_name)
|
||||
|
||||
self._attr_is_opening = False
|
||||
self._attr_is_closing = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.STOP)
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Set the cover open position in percentage.
|
||||
|
||||
Closing or opening status is effective after the server callback that triggers callback_device_state.
|
||||
"""
|
||||
position: int = kwargs[ATTR_POSITION]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Set cover position %i, %s , %s", position, self.target_id, self._attr_name
|
||||
)
|
||||
|
||||
await self.client.move_shutter_percentage(self.target_id, position)
|
53
homeassistant/components/chacon_dio/entity.py
Normal file
53
homeassistant/components/chacon_dio/entity.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""Base entity for the Chacon Dio entity."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChaconDioEntity(Entity):
|
||||
"""Implements a common class elements representing the Chacon Dio entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, client: DIOChaconAPIClient, device: dict[str, Any]) -> None:
|
||||
"""Initialize Chacon Dio entity."""
|
||||
|
||||
self.client = client
|
||||
|
||||
self.target_id: str = device["id"]
|
||||
self._attr_unique_id = self.target_id
|
||||
self._attr_device_info: DeviceInfo | None = DeviceInfo(
|
||||
identifiers={(DOMAIN, self.target_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=device["name"],
|
||||
model=device["model"],
|
||||
)
|
||||
|
||||
self._update_attr(device)
|
||||
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recomputes the attributes values."""
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register the callback for server side events."""
|
||||
await super().async_added_to_hass()
|
||||
self.client.set_callback_device_state_by_device(
|
||||
self.target_id, self.callback_device_state
|
||||
)
|
||||
|
||||
def callback_device_state(self, data: dict[str, Any]) -> None:
|
||||
"""Receive callback for device state notification pushed from the server."""
|
||||
|
||||
_LOGGER.debug("Data received from server %s", data)
|
||||
self._update_attr(data)
|
||||
self.async_write_ha_state()
|
10
homeassistant/components/chacon_dio/manifest.json
Normal file
10
homeassistant/components/chacon_dio/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "chacon_dio",
|
||||
"name": "Chacon DiO",
|
||||
"codeowners": ["@cnico"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.1.0"]
|
||||
}
|
20
homeassistant/components/chacon_dio/strings.json
Normal file
20
homeassistant/components/chacon_dio/strings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -98,6 +98,7 @@ FLOWS = {
|
||||
"cast",
|
||||
"ccm15",
|
||||
"cert_expiry",
|
||||
"chacon_dio",
|
||||
"cloudflare",
|
||||
"co2signal",
|
||||
"coinbase",
|
||||
|
@ -878,6 +878,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"chacon_dio": {
|
||||
"name": "Chacon DiO",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"channels": {
|
||||
"name": "Channels",
|
||||
"integration_type": "hub",
|
||||
|
@ -734,6 +734,9 @@ devolo-home-control-api==0.18.3
|
||||
# homeassistant.components.devolo_home_network
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.chacon_dio
|
||||
dio-chacon-wifi-api==1.1.0
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
|
||||
|
@ -615,6 +615,9 @@ devolo-home-control-api==0.18.3
|
||||
# homeassistant.components.devolo_home_network
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.chacon_dio
|
||||
dio-chacon-wifi-api==1.1.0
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
|
||||
|
13
tests/components/chacon_dio/__init__.py
Normal file
13
tests/components/chacon_dio/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Tests for the Chacon Dio integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
71
tests/components/chacon_dio/conftest.py
Normal file
71
tests/components/chacon_dio/conftest.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Common fixtures for the chacon_dio tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.chacon_dio.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_COVER_DEVICE = {
|
||||
"L4HActuator_idmock1": {
|
||||
"id": "L4HActuator_idmock1",
|
||||
"name": "Shutter mock 1",
|
||||
"type": "SHUTTER",
|
||||
"model": "CERSwd-3B_1.0.6",
|
||||
"connected": True,
|
||||
"openlevel": 75,
|
||||
"movement": "stop",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.chacon_dio.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock the config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="test_entry_unique_id",
|
||||
data={
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dio_chacon_client() -> Generator[AsyncMock]:
|
||||
"""Mock a Dio Chacon client."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.chacon_dio.DIOChaconAPIClient",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.chacon_dio.config_flow.DIOChaconAPIClient",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
|
||||
# Default values for the tests using this mock :
|
||||
client.get_user_id.return_value = "dummy-user-id"
|
||||
client.search_all_devices.return_value = MOCK_COVER_DEVICE
|
||||
|
||||
client.move_shutter_direction.return_value = {}
|
||||
client.disconnect.return_value = {}
|
||||
|
||||
yield client
|
50
tests/components/chacon_dio/snapshots/test_cover.ambr
Normal file
50
tests/components/chacon_dio/snapshots/test_cover.ambr
Normal file
@ -0,0 +1,50 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[cover.shutter_mock_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'cover',
|
||||
'entity_category': None,
|
||||
'entity_id': 'cover.shutter_mock_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <CoverDeviceClass.SHUTTER: 'shutter'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'chacon_dio',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'L4HActuator_idmock1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[cover.shutter_mock_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_position': 75,
|
||||
'device_class': 'shutter',
|
||||
'friendly_name': 'Shutter mock 1',
|
||||
'supported_features': <CoverEntityFeature: 15>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'cover.shutter_mock_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'open',
|
||||
})
|
||||
# ---
|
122
tests/components/chacon_dio/test_config_flow.py
Normal file
122
tests/components/chacon_dio/test_config_flow.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""Test the chacon_dio config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.chacon_dio.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_dio_chacon_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test the full flow."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert not result["errors"]
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Chacon DiO dummylogin"
|
||||
assert result["result"].unique_id == "dummy-user-id"
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected"),
|
||||
[
|
||||
(Exception("Bad request Boy :) --"), {"base": "unknown"}),
|
||||
(DIOChaconInvalidAuthError, {"base": "invalid_auth"}),
|
||||
(DIOChaconAPIError, {"base": "cannot_connect"}),
|
||||
],
|
||||
)
|
||||
async def test_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
exception: Exception,
|
||||
expected: dict[str, str],
|
||||
) -> None:
|
||||
"""Test we handle any error."""
|
||||
mock_dio_chacon_client.get_user_id.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={
|
||||
CONF_USERNAME: "nada",
|
||||
CONF_PASSWORD: "nadap",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == expected
|
||||
|
||||
# Test of recover in normal state after correction of the 1st error
|
||||
mock_dio_chacon_client.get_user_id.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Chacon DiO dummylogin"
|
||||
assert result["result"].unique_id == "dummy-user-id"
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
}
|
||||
|
||||
|
||||
async def test_duplicate_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test abort when setting up duplicate entry."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
mock_dio_chacon_client.get_user_id.return_value = "test_entry_unique_id"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "dummylogin",
|
||||
CONF_PASSWORD: "dummypass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
157
tests/components/chacon_dio/test_cover.py
Normal file
157
tests/components/chacon_dio/test_cover.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""Test the Chacon Dio cover."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
COVER_ENTITY_ID = "cover.shutter_mock_1"
|
||||
|
||||
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the creation and values of the Chacon Dio covers."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_cover_actions(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the creation and values of the Chacon Dio covers."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_CLOSE_COVER,
|
||||
{ATTR_ENTITY_ID: COVER_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state.state == STATE_CLOSING
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_STOP_COVER,
|
||||
{ATTR_ENTITY_ID: COVER_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_OPEN_COVER,
|
||||
{ATTR_ENTITY_ID: COVER_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
{ATTR_POSITION: 25, ATTR_ENTITY_ID: COVER_ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
|
||||
async def test_cover_callbacks(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the creation and values of the Chacon Dio covers."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Server side callback tests
|
||||
# We find the callback method on the mock client
|
||||
callback_device_state_function: Callable = (
|
||||
mock_dio_chacon_client.set_callback_device_state_by_device.call_args[0][1]
|
||||
)
|
||||
|
||||
# Define a method to simply call it
|
||||
async def _callback_device_state_function(open_level: int, movement: str) -> None:
|
||||
callback_device_state_function(
|
||||
{
|
||||
"id": "L4HActuator_idmock1",
|
||||
"connected": True,
|
||||
"openlevel": open_level,
|
||||
"movement": movement,
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# And call it to effectively launch the callback as the server would do
|
||||
await _callback_device_state_function(79, "stop")
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_CURRENT_POSITION) == 79
|
||||
assert state.state == STATE_OPEN
|
||||
|
||||
await _callback_device_state_function(90, "up")
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_CURRENT_POSITION) == 90
|
||||
assert state.state == STATE_OPENING
|
||||
|
||||
await _callback_device_state_function(60, "down")
|
||||
state = hass.states.get(COVER_ENTITY_ID)
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_CURRENT_POSITION) == 60
|
||||
assert state.state == STATE_CLOSING
|
||||
|
||||
|
||||
async def test_no_cover_found(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the cover absence."""
|
||||
|
||||
mock_dio_chacon_client.search_all_devices.return_value = None
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert not hass.states.get(COVER_ENTITY_ID)
|
43
tests/components/chacon_dio/test_init.py
Normal file
43
tests/components/chacon_dio/test_init.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Test the Dio Chacon Cover init."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_cover_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the creation and values of the Dio Chacon covers."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
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
|
||||
mock_dio_chacon_client.disconnect.assert_called()
|
||||
|
||||
|
||||
async def test_cover_shutdown_event(
|
||||
hass: HomeAssistant,
|
||||
mock_dio_chacon_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the creation and values of the Dio Chacon covers."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
mock_dio_chacon_client.disconnect.assert_called()
|
Loading…
x
Reference in New Issue
Block a user