Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Deako integration #121132

Merged
merged 6 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,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
Expand Down
64 changes: 64 additions & 0 deletions homeassistant/components/deako/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""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]


class DeakoConfigEntry(ConfigEntry):
"""Typed config entry."""

runtime_data: Deako | None
Balake marked this conversation as resolved.
Show resolved Hide resolved


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."""
if entry.runtime_data is not None:
await entry.runtime_data.disconnect()
Balake marked this conversation as resolved.
Show resolved Hide resolved
Balake marked this conversation as resolved.
Show resolved Hide resolved

return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
26 changes: 26 additions & 0 deletions homeassistant/components/deako/config_flow.py
Original file line number Diff line number Diff line change
@@ -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()
# address exists, there's at least one device
return True

except DevicesNotFoundException:
return False


config_entry_flow.register_discovery_flow(DOMAIN, NAME, _async_has_devices)
5 changes: 5 additions & 0 deletions homeassistant/components/deako/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for Deako."""

# Base component constants
NAME = "Deako"
DOMAIN = "deako"
106 changes: 106 additions & 0 deletions homeassistant/components/deako/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Binary sensor platform for integration_blueprint."""

import logging
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"

_LOGGER: logging.Logger = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config: DeakoConfigEntry,
add_entities: AddEntitiesCallback,
) -> None:
"""Configure the platform."""
client = config.runtime_data

if client is not None:
add_entities([DeakoLightEntity(client, uuid) for uuid in client.get_devices()])
else:
_LOGGER.error("Deako client not set in config entry")
Balake marked this conversation as resolved.
Show resolved Hide resolved


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()

def get_state(self) -> dict[str, bool | int]:
"""Return state of entity from client."""
assert self._attr_unique_id is not None
return self.client.get_state(self._attr_unique_id) or {}

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."""
state = self.get_state()
Balake marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 13 additions & 0 deletions homeassistant/components/deako/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
Balake marked this conversation as resolved.
Show resolved Hide resolved
"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."]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering, does zeroconf now work properly? As in, does it propose you add the intergation and then start the discovery flow?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does!

Screenshot 2024-08-12 at 12 57 38 PM

}
13 changes: 13 additions & 0 deletions homeassistant/components/deako/strings.json
Original file line number Diff line number Diff line change
@@ -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%]"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"cpuspeed",
"crownstone",
"daikin",
"deako",
"deconz",
"deluge",
"denonavr",
Expand Down
7 changes: 7 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,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",
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/generated/zeroconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@
"domain": "forked_daapd",
},
],
"_deako._tcp.local.": [
{
"domain": "deako",
},
],
"_devialet-http._tcp.local.": [
{
"domain": "devialet",
Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,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
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,9 @@ pydaikin==2.13.1
# homeassistant.components.danfoss_air
pydanfossair==0.1.0

# homeassistant.components.deako
pydeako==0.4.0

# homeassistant.components.deconz
pydeconz==116

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,9 @@ pycsspeechtts==1.0.8
# homeassistant.components.daikin
pydaikin==2.13.1

# homeassistant.components.deako
pydeako==0.4.0

# homeassistant.components.deconz
pydeconz==116

Expand Down
1 change: 1 addition & 0 deletions tests/components/deako/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Deako integration."""
45 changes: 45 additions & 0 deletions tests/components/deako/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Loading