mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add Deako integration (#121132)
* Deako integration using pydeako * fix: address feedback - make unit tests more e2e - use runtime_data to store connection * fix: address feedback part 2 - added better type safety for Deako config entries - refactored the config flow tests to use a conftest mock instead of directly patching - removed pytest.mark.asyncio test decorators * fix: address feedback pt 3 - simplify config entry type - add test for single_instance_allowed - remove light.py get_state(), only used once, no need to be separate function * fix: ruff format * Update homeassistant/components/deako/__init__.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@ -139,6 +139,7 @@ homeassistant.components.cpuspeed.*
|
||||
homeassistant.components.crownstone.*
|
||||
homeassistant.components.date.*
|
||||
homeassistant.components.datetime.*
|
||||
homeassistant.components.deako.*
|
||||
homeassistant.components.deconz.*
|
||||
homeassistant.components.default_config.*
|
||||
homeassistant.components.demo.*
|
||||
|
@ -294,6 +294,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/date/ @home-assistant/core
|
||||
/homeassistant/components/datetime/ @home-assistant/core
|
||||
/tests/components/datetime/ @home-assistant/core
|
||||
/homeassistant/components/deako/ @sebirdman @balake @deakolights
|
||||
/tests/components/deako/ @sebirdman @balake @deakolights
|
||||
/homeassistant/components/debugpy/ @frenck
|
||||
/tests/components/debugpy/ @frenck
|
||||
/homeassistant/components/deconz/ @Kane610
|
||||
|
59
homeassistant/components/deako/__init__.py
Normal file
59
homeassistant/components/deako/__init__.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""The deako integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout
|
||||
from pydeako.discover import DeakoDiscoverer
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
type DeakoConfigEntry = ConfigEntry[Deako]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> bool:
|
||||
"""Set up deako."""
|
||||
_zc = await zeroconf.async_get_instance(hass)
|
||||
discoverer = DeakoDiscoverer(_zc)
|
||||
|
||||
connection = Deako(discoverer.get_address)
|
||||
|
||||
await connection.connect()
|
||||
try:
|
||||
await connection.find_devices()
|
||||
except DeviceListTimeout as exc: # device list never received
|
||||
_LOGGER.warning("Device not responding to device list")
|
||||
await connection.disconnect()
|
||||
raise ConfigEntryNotReady(exc) from exc
|
||||
except FindDevicesTimeout as exc: # total devices expected not received
|
||||
_LOGGER.warning("Device not responding to device requests")
|
||||
await connection.disconnect()
|
||||
raise ConfigEntryNotReady(exc) from exc
|
||||
|
||||
# If deako devices are advertising on mdns, we should be able to get at least one device
|
||||
devices = connection.get_devices()
|
||||
if len(devices) == 0:
|
||||
await connection.disconnect()
|
||||
raise ConfigEntryNotReady(devices)
|
||||
|
||||
entry.runtime_data = connection
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DeakoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.disconnect()
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
26
homeassistant/components/deako/config_flow.py
Normal file
26
homeassistant/components/deako/config_flow.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""Config flow for deako."""
|
||||
|
||||
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import DOMAIN, NAME
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
"""Return if there are devices that can be discovered."""
|
||||
_zc = await zeroconf.async_get_instance(hass)
|
||||
discoverer = DeakoDiscoverer(_zc)
|
||||
|
||||
try:
|
||||
await discoverer.get_address()
|
||||
except DevicesNotFoundException:
|
||||
return False
|
||||
else:
|
||||
# address exists, there's at least one device
|
||||
return True
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(DOMAIN, NAME, _async_has_devices)
|
5
homeassistant/components/deako/const.py
Normal file
5
homeassistant/components/deako/const.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Constants for Deako."""
|
||||
|
||||
# Base component constants
|
||||
NAME = "Deako"
|
||||
DOMAIN = "deako"
|
96
homeassistant/components/deako/light.py
Normal file
96
homeassistant/components/deako/light.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""Binary sensor platform for integration_blueprint."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydeako.deako import Deako
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import DeakoConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
# Model names
|
||||
MODEL_SMART = "smart"
|
||||
MODEL_DIMMER = "dimmer"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: DeakoConfigEntry,
|
||||
add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure the platform."""
|
||||
client = config.runtime_data
|
||||
|
||||
add_entities([DeakoLightEntity(client, uuid) for uuid in client.get_devices()])
|
||||
|
||||
|
||||
class DeakoLightEntity(LightEntity):
|
||||
"""Deako LightEntity class."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_is_on = False
|
||||
_attr_available = True
|
||||
|
||||
client: Deako
|
||||
|
||||
def __init__(self, client: Deako, uuid: str) -> None:
|
||||
"""Save connection reference."""
|
||||
self.client = client
|
||||
self._attr_unique_id = uuid
|
||||
|
||||
dimmable = client.is_dimmable(uuid)
|
||||
|
||||
model = MODEL_SMART
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
if dimmable:
|
||||
model = MODEL_DIMMER
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
|
||||
self._attr_supported_color_modes = {self._attr_color_mode}
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, uuid)},
|
||||
name=client.get_name(uuid),
|
||||
manufacturer="Deako",
|
||||
model=model,
|
||||
)
|
||||
|
||||
client.set_state_callback(uuid, self.on_update)
|
||||
self.update() # set initial state
|
||||
|
||||
def on_update(self) -> None:
|
||||
"""State update callback."""
|
||||
self.update()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
async def control_device(self, power: bool, dim: int | None = None) -> None:
|
||||
"""Control entity state via client."""
|
||||
assert self._attr_unique_id is not None
|
||||
await self.client.control_device(self._attr_unique_id, power, dim)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
dim = None
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
dim = round(kwargs[ATTR_BRIGHTNESS] / 2.55, 0)
|
||||
await self.control_device(True, dim)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the device."""
|
||||
await self.control_device(False)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Call to update state."""
|
||||
assert self._attr_unique_id is not None
|
||||
state = self.client.get_state(self._attr_unique_id) or {}
|
||||
self._attr_is_on = bool(state.get("power", False))
|
||||
if (
|
||||
self._attr_supported_color_modes is not None
|
||||
and ColorMode.BRIGHTNESS in self._attr_supported_color_modes
|
||||
):
|
||||
self._attr_brightness = int(round(state.get("dim", 0) * 2.55))
|
13
homeassistant/components/deako/manifest.json
Normal file
13
homeassistant/components/deako/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "deako",
|
||||
"name": "Deako",
|
||||
"codeowners": ["@sebirdman", "@balake", "@deakolights"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["zeroconf"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/deako",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydeako"],
|
||||
"requirements": ["pydeako==0.4.0"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_deako._tcp.local."]
|
||||
}
|
13
homeassistant/components/deako/strings.json
Normal file
13
homeassistant/components/deako/strings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Please confirm setting up the Deako integration"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -114,6 +114,7 @@ FLOWS = {
|
||||
"cpuspeed",
|
||||
"crownstone",
|
||||
"daikin",
|
||||
"deako",
|
||||
"deconz",
|
||||
"deluge",
|
||||
"denonavr",
|
||||
|
@ -1091,6 +1091,13 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"deako": {
|
||||
"name": "Deako",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"single_config_entry": true
|
||||
},
|
||||
"debugpy": {
|
||||
"name": "Remote Python Debugger",
|
||||
"integration_type": "service",
|
||||
|
@ -423,6 +423,11 @@ ZEROCONF = {
|
||||
"domain": "forked_daapd",
|
||||
},
|
||||
],
|
||||
"_deako._tcp.local.": [
|
||||
{
|
||||
"domain": "deako",
|
||||
},
|
||||
],
|
||||
"_devialet-http._tcp.local.": [
|
||||
{
|
||||
"domain": "devialet",
|
||||
|
10
mypy.ini
10
mypy.ini
@ -1145,6 +1145,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.deako.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.deconz.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -1803,6 +1803,9 @@ pydaikin==2.13.4
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
|
||||
# homeassistant.components.deako
|
||||
pydeako==0.4.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==116
|
||||
|
||||
|
@ -1447,6 +1447,9 @@ pycsspeechtts==1.0.8
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.13.4
|
||||
|
||||
# homeassistant.components.deako
|
||||
pydeako==0.4.0
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==116
|
||||
|
||||
|
1
tests/components/deako/__init__.py
Normal file
1
tests/components/deako/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Deako integration."""
|
45
tests/components/deako/conftest.py
Normal file
45
tests/components/deako/conftest.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""deako session fixtures."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.deako.const import DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def pydeako_deako_mock() -> Generator[MagicMock]:
|
||||
"""Mock pydeako deako client."""
|
||||
with patch("homeassistant.components.deako.Deako", autospec=True) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def pydeako_discoverer_mock(mock_async_zeroconf: MagicMock) -> Generator[MagicMock]:
|
||||
"""Mock pydeako discovery client."""
|
||||
with (
|
||||
patch("homeassistant.components.deako.DeakoDiscoverer", autospec=True) as mock,
|
||||
patch("homeassistant.components.deako.config_flow.DeakoDiscoverer", new=mock),
|
||||
):
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_deako_setup() -> Generator[MagicMock]:
|
||||
"""Mock async_setup_entry for config flow tests."""
|
||||
with patch(
|
||||
"homeassistant.components.deako.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
168
tests/components/deako/snapshots/test_light.ambr
Normal file
168
tests/components/deako/snapshots/test_light.ambr
Normal file
@ -0,0 +1,168 @@
|
||||
# serializer version: 1
|
||||
# name: test_dimmable_light_props[light.kitchen-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.kitchen',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'deako',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'uuid',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_dimmable_light_props[light.kitchen-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': 127,
|
||||
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
'friendly_name': 'kitchen',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.kitchen',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_light_initial_props[light.kitchen-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.kitchen',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'deako',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'uuid',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_light_initial_props[light.kitchen-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': None,
|
||||
'friendly_name': 'kitchen',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.kitchen',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_light_setup_with_device[light.some_device-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.some_device',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'deako',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'some_device',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_light_setup_with_device[light.some_device-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': 1,
|
||||
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
'friendly_name': 'some device',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.some_device',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
80
tests/components/deako/test_config_flow.py
Normal file
80
tests/components/deako/test_config_flow.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Tests for the deako component config flow."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pydeako.discover import DevicesNotFoundException
|
||||
|
||||
from homeassistant.components.deako.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_found(
|
||||
hass: HomeAssistant,
|
||||
pydeako_discoverer_mock: MagicMock,
|
||||
mock_deako_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test finding a Deako device."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
pydeako_discoverer_mock.return_value.get_address.assert_called_once()
|
||||
|
||||
mock_deako_setup.assert_called_once()
|
||||
|
||||
|
||||
async def test_not_found(
|
||||
hass: HomeAssistant,
|
||||
pydeako_discoverer_mock: MagicMock,
|
||||
mock_deako_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test not finding any Deako devices."""
|
||||
pydeako_discoverer_mock.return_value.get_address.side_effect = (
|
||||
DevicesNotFoundException()
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
pydeako_discoverer_mock.return_value.get_address.assert_called_once()
|
||||
|
||||
mock_deako_setup.assert_not_called()
|
||||
|
||||
|
||||
async def test_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_deako_setup: MagicMock,
|
||||
) -> None:
|
||||
"""Test flow aborts when already configured."""
|
||||
|
||||
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.ABORT
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
mock_deako_setup.assert_not_called()
|
87
tests/components/deako/test_init.py
Normal file
87
tests/components/deako/test_init.py
Normal file
@ -0,0 +1,87 @@
|
||||
"""Tests for the deako component init."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pydeako.deako import DeviceListTimeout, FindDevicesTimeout
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_deako_async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
pydeako_discoverer_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test successful setup entry."""
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"id1": {},
|
||||
"id2": {},
|
||||
}
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
pydeako_deako_mock.assert_called_once_with(
|
||||
pydeako_discoverer_mock.return_value.get_address
|
||||
)
|
||||
pydeako_deako_mock.return_value.connect.assert_called_once()
|
||||
pydeako_deako_mock.return_value.find_devices.assert_called_once()
|
||||
pydeako_deako_mock.return_value.get_devices.assert_called()
|
||||
|
||||
assert mock_config_entry.runtime_data == pydeako_deako_mock.return_value
|
||||
|
||||
|
||||
async def test_deako_async_setup_entry_device_list_timeout(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
pydeako_discoverer_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test async_setup_entry raises ConfigEntryNotReady when pydeako raises DeviceListTimeout."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.find_devices.side_effect = DeviceListTimeout()
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
pydeako_deako_mock.assert_called_once_with(
|
||||
pydeako_discoverer_mock.return_value.get_address
|
||||
)
|
||||
pydeako_deako_mock.return_value.connect.assert_called_once()
|
||||
pydeako_deako_mock.return_value.find_devices.assert_called_once()
|
||||
pydeako_deako_mock.return_value.disconnect.assert_called_once()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_deako_async_setup_entry_find_devices_timeout(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
pydeako_discoverer_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test async_setup_entry raises ConfigEntryNotReady when pydeako raises FindDevicesTimeout."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.find_devices.side_effect = FindDevicesTimeout()
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
pydeako_deako_mock.assert_called_once_with(
|
||||
pydeako_discoverer_mock.return_value.get_address
|
||||
)
|
||||
pydeako_deako_mock.return_value.connect.assert_called_once()
|
||||
pydeako_deako_mock.return_value.find_devices.assert_called_once()
|
||||
pydeako_deako_mock.return_value.disconnect.assert_called_once()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
192
tests/components/deako/test_light.py
Normal file
192
tests/components/deako/test_light.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Tests for the light module."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_light_setup_with_device(
|
||||
hass: HomeAssistant,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test light platform setup with device returned."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"some_device": {},
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "some device"
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_light_initial_props(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test on/off light is setup with accurate initial properties."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"uuid": {
|
||||
"name": "kitchen",
|
||||
}
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "kitchen"
|
||||
pydeako_deako_mock.return_value.get_state.return_value = {
|
||||
"power": False,
|
||||
}
|
||||
pydeako_deako_mock.return_value.is_dimmable.return_value = False
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_dimmable_light_props(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test dimmable on/off light is setup with accurate initial properties."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"uuid": {
|
||||
"name": "kitchen",
|
||||
}
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "kitchen"
|
||||
pydeako_deako_mock.return_value.get_state.return_value = {
|
||||
"power": True,
|
||||
"dim": 50,
|
||||
}
|
||||
pydeako_deako_mock.return_value.is_dimmable.return_value = True
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_light_power_change_on(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test turing on a deako device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"uuid": {
|
||||
"name": "kitchen",
|
||||
}
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "kitchen"
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.kitchen"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
pydeako_deako_mock.return_value.control_device.assert_called_once_with(
|
||||
"uuid", True, None
|
||||
)
|
||||
|
||||
|
||||
async def test_light_power_change_off(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test turing off a deako device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"uuid": {
|
||||
"name": "kitchen",
|
||||
}
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "kitchen"
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.kitchen"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
pydeako_deako_mock.return_value.control_device.assert_called_once_with(
|
||||
"uuid", False, None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dim_input", "expected_dim_value"),
|
||||
[
|
||||
(3, 1),
|
||||
(255, 100),
|
||||
(127, 50),
|
||||
],
|
||||
)
|
||||
async def test_light_brightness_change(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
pydeako_deako_mock: MagicMock,
|
||||
dim_input: int,
|
||||
expected_dim_value: int,
|
||||
) -> None:
|
||||
"""Test turing on a deako device."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
pydeako_deako_mock.return_value.get_devices.return_value = {
|
||||
"uuid": {
|
||||
"name": "kitchen",
|
||||
}
|
||||
}
|
||||
pydeako_deako_mock.return_value.get_name.return_value = "kitchen"
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: "light.kitchen",
|
||||
ATTR_BRIGHTNESS: dim_input,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
pydeako_deako_mock.return_value.control_device.assert_called_once_with(
|
||||
"uuid", True, expected_dim_value
|
||||
)
|
Reference in New Issue
Block a user