mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00: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:
parent
2dce876a86
commit
c049129147
@ -139,6 +139,7 @@ homeassistant.components.cpuspeed.*
|
|||||||
homeassistant.components.crownstone.*
|
homeassistant.components.crownstone.*
|
||||||
homeassistant.components.date.*
|
homeassistant.components.date.*
|
||||||
homeassistant.components.datetime.*
|
homeassistant.components.datetime.*
|
||||||
|
homeassistant.components.deako.*
|
||||||
homeassistant.components.deconz.*
|
homeassistant.components.deconz.*
|
||||||
homeassistant.components.default_config.*
|
homeassistant.components.default_config.*
|
||||||
homeassistant.components.demo.*
|
homeassistant.components.demo.*
|
||||||
|
@ -294,6 +294,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/date/ @home-assistant/core
|
/tests/components/date/ @home-assistant/core
|
||||||
/homeassistant/components/datetime/ @home-assistant/core
|
/homeassistant/components/datetime/ @home-assistant/core
|
||||||
/tests/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
|
/homeassistant/components/debugpy/ @frenck
|
||||||
/tests/components/debugpy/ @frenck
|
/tests/components/debugpy/ @frenck
|
||||||
/homeassistant/components/deconz/ @Kane610
|
/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",
|
"cpuspeed",
|
||||||
"crownstone",
|
"crownstone",
|
||||||
"daikin",
|
"daikin",
|
||||||
|
"deako",
|
||||||
"deconz",
|
"deconz",
|
||||||
"deluge",
|
"deluge",
|
||||||
"denonavr",
|
"denonavr",
|
||||||
|
@ -1091,6 +1091,13 @@
|
|||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
|
"deako": {
|
||||||
|
"name": "Deako",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"single_config_entry": true
|
||||||
|
},
|
||||||
"debugpy": {
|
"debugpy": {
|
||||||
"name": "Remote Python Debugger",
|
"name": "Remote Python Debugger",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
|
@ -423,6 +423,11 @@ ZEROCONF = {
|
|||||||
"domain": "forked_daapd",
|
"domain": "forked_daapd",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"_deako._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "deako",
|
||||||
|
},
|
||||||
|
],
|
||||||
"_devialet-http._tcp.local.": [
|
"_devialet-http._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "devialet",
|
"domain": "devialet",
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -1145,6 +1145,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.deconz.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -1803,6 +1803,9 @@ pydaikin==2.13.4
|
|||||||
# homeassistant.components.danfoss_air
|
# homeassistant.components.danfoss_air
|
||||||
pydanfossair==0.1.0
|
pydanfossair==0.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.deako
|
||||||
|
pydeako==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==116
|
pydeconz==116
|
||||||
|
|
||||||
|
@ -1447,6 +1447,9 @@ pycsspeechtts==1.0.8
|
|||||||
# homeassistant.components.daikin
|
# homeassistant.components.daikin
|
||||||
pydaikin==2.13.4
|
pydaikin==2.13.4
|
||||||
|
|
||||||
|
# homeassistant.components.deako
|
||||||
|
pydeako==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.deconz
|
# homeassistant.components.deconz
|
||||||
pydeconz==116
|
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
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user