Skip to content

Commit

Permalink
Deako integration using pydeako
Browse files Browse the repository at this point in the history
  • Loading branch information
Balake committed Jul 11, 2024
1 parent e269ff6 commit 8354ae4
Show file tree
Hide file tree
Showing 19 changed files with 1,080 additions and 0 deletions.
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
65 changes: 65 additions & 0 deletions homeassistant/components/deako/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""The deako integration."""

from __future__ import annotations

import logging

from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException

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

from .const import ADDRESS, CONNECTION, DOMAIN

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

PLATFORMS: list[Platform] = [Platform.LIGHT]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up deako."""
# reuse connection from discovery if it exists
connection = (hass.data.get(DOMAIN) or {}).get(CONNECTION)

if connection is None:
address = entry.data.get(ADDRESS)
if address is not None:

async def get_address() -> str:
assert isinstance(address, str)
return address
else:
_zc = await zeroconf.async_get_instance(hass)
discoverer = DeakoDiscoverer(_zc)
get_address = discoverer.get_address

connection = Deako(get_address)

try:
await connection.connect()
await connection.find_devices()
except (DevicesNotFoundException, DeviceListTimeout, FindDevicesTimeout) as exc:
await connection.disconnect()
raise ConfigEntryNotReady(exc) from exc

hass.data.setdefault(DOMAIN, {})

hass.data[DOMAIN][CONNECTION] = connection

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
await hass.data[DOMAIN][CONNECTION].disconnect()

if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data.pop(DOMAIN)

return unload_ok
126 changes: 126 additions & 0 deletions homeassistant/components/deako/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Config flow for deako."""

import logging
from typing import Any

from pydeako.deako import Deako, DeviceListTimeout, FindDevicesTimeout
from pydeako.discover import DeakoDiscoverer, DevicesNotFoundException
import voluptuous as vol

from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.core import callback

from .const import ADDRESS, CONNECTION, DEFAULT_PORT, DOMAIN

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


class DeakoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Deako integration config flow."""

VERSION = 1
connection: Deako | None = None

address: str | None = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
if user_input is None:
return self._async_show_setup_form()

await self.async_set_unique_id(DOMAIN)
host = user_input.get(CONF_HOST)
if host is not None:
self.address = f"{host}:{DEFAULT_PORT}"
self._abort_if_unique_id_configured(
updates={ADDRESS: self.address},
reload_on_update=True,
)
else:
self._abort_if_unique_id_configured()

return await self._finalize()

async def async_step_zeroconf(
self, discovery_info: zeroconf.ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
# only one instance of deako integration should exist, device is a bridge
if self.hass.data.get(DOMAIN) is not None:
return self.async_abort(reason="already_configured")

await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
self._set_confirm_only()
return self.async_show_form(
step_id="zeroconf_confirm",
)

async def async_step_zeroconf_confirm(
self, _: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by zeroconf."""
return await self._finalize()

@callback
def _async_create_entry(self) -> ConfigFlowResult:
data: dict[str, Any] = {}

if self.address is not None:
data[ADDRESS] = self.address

self.hass.data.setdefault(
DOMAIN,
{
CONNECTION: self.connection, # let setup reuse connection
},
)

return self.async_create_entry(
title="Deako integration",
data=data,
)

@callback
def _async_show_setup_form(
self, errors: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Show the setup form to the user."""
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Optional(CONF_HOST): str,
}
),
errors=errors or {},
)

async def _finalize(self) -> ConfigFlowResult:
if self.address is not None:
address = self.address

async def get_address() -> str:
return address
else:
_zc = await zeroconf.async_get_instance(self.hass)
discoverer = DeakoDiscoverer(_zc)
get_address = discoverer.get_address

connection = Deako(get_address)

try:
await connection.connect()
await connection.find_devices()
self.connection = connection
except (DevicesNotFoundException, DeviceListTimeout, FindDevicesTimeout) as exc:
_LOGGER.error(exc)
await connection.disconnect()
await self.async_set_unique_id()
return self.async_abort(reason="cannot_connect")

return self._async_create_entry()
10 changes: 10 additions & 0 deletions homeassistant/components/deako/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Constants for Deako."""

# Base component constants
NAME = "Deako"
DOMAIN = "deako"

# Connection
DEFAULT_PORT = 23
CONNECTION = "connection"
ADDRESS = "address"
102 changes: 102 additions & 0 deletions homeassistant/components/deako/light.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""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.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import CONNECTION, DOMAIN

# Model names
MODEL_SMART = "smart"
MODEL_DIMMER = "dimmer"

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


async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
add_entities: AddEntitiesCallback,
) -> None:
"""Configure the platform."""
client: Deako = hass.data[DOMAIN][CONNECTION]

devices = client.get_devices()
lights = [DeakoLightEntity(client, uuid) for uuid in devices]
add_entities(lights)


class DeakoLightEntity(LightEntity):
"""Deako LightEntity class."""

# retype because these will be set
_attr_unique_id: str
_attr_supported_color_modes: set[ColorMode]

_attr_has_entity_name = True
_attr_name = None
_attr_is_on = False
_attr_available = True

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:
"""Return state of entity from client."""
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."""
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()
self._attr_is_on = bool(state.get("power", False))
if ColorMode.BRIGHTNESS in self._attr_supported_color_modes:
self._attr_brightness = int(round(state.get("dim", 0) * 2.55))
12 changes: 12 additions & 0 deletions homeassistant/components/deako/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"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"],
"zeroconf": ["_deako._tcp.local."]
}
19 changes: 19 additions & 0 deletions homeassistant/components/deako/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"config": {
"step": {
"user": {
"description": "Set up Deako to integrate with Home Assistant.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "Host for connected Deako device (static local IP preferred). Leave blank to use auto-discovery."
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Deako integration to Home Assistant?",
"title": "Discovered Deako device"
}
}
}
}
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
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,12 @@
"config_flow": false,
"iot_class": "local_polling"
},
"deako": {
"name": "Deako",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"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
Loading

0 comments on commit 8354ae4

Please sign in to comment.