Skip to content

Commit

Permalink
Add new integration dyson_local
Browse files Browse the repository at this point in the history
  • Loading branch information
shenxn committed Aug 1, 2021
1 parent b3f0d68 commit a84106e
Show file tree
Hide file tree
Showing 19 changed files with 1,128 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234
homeassistant/components/dyson_local/* @shenxn
homeassistant/components/eafm/* @Jc2k
homeassistant/components/ecobee/* @marthoc
homeassistant/components/econet/* @vangorra @w1ll1am23
Expand Down
117 changes: 117 additions & 0 deletions homeassistant/components/dyson_local/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Support for Dyson devices."""
from __future__ import annotations

from libdyson import MessageType, get_device
from libdyson.dyson_device import DysonDevice
from libdyson.exceptions import DysonException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo, Entity

from .const import CONF_CREDENTIAL, CONF_DEVICE_TYPE, CONF_SERIAL, DATA_DEVICES, DOMAIN

_PLATFORMS = ["binary_sensor", "sensor", "vacuum"]


async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up Dyson integration."""
hass.data[DOMAIN] = {
DATA_DEVICES: {},
}
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Dyson from a config entry."""
device = get_device(
entry.data[CONF_SERIAL],
entry.data[CONF_CREDENTIAL],
entry.data[CONF_DEVICE_TYPE],
)

try:
await hass.async_add_executor_job(device.connect, entry.data[CONF_HOST])
except DysonException as err:
raise ConfigEntryNotReady from err
hass.data[DOMAIN][DATA_DEVICES][entry.entry_id] = device
for component in _PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Dyson local."""
device = hass.data[DOMAIN][DATA_DEVICES][entry.entry_id]
unload_ok = await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
if unload_ok:
hass.data[DOMAIN][DATA_DEVICES].pop(entry.entry_id)
await hass.async_add_executor_job(device.disconnect)
return unload_ok


class DysonEntity(Entity):
"""Dyson entity base class."""

_MESSAGE_TYPE = MessageType.STATE

def __init__(self, device: DysonDevice, name: str) -> None:
"""Initialize the entity."""
self._device = device
self._name = name

async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
self._device.add_message_listener(self._on_message)

async def async_will_remove_from_hass(self) -> None:
"""Call when entity to be removed from hass."""
self._device.remove_message_listener(self._on_message)

def _on_message(self, message_type: MessageType) -> None:
if self._MESSAGE_TYPE is None or message_type == self._MESSAGE_TYPE:
self.schedule_update_ha_state()

@property
def should_poll(self) -> bool:
"""No polling needed."""
return False

@property
def name(self) -> str:
"""Return the name of the entity."""
if self.sub_name is None:
return self._name
return f"{self._name} {self.sub_name}"

@property
def sub_name(self) -> str | None:
"""Return sub name of the entity."""
return None

@property
def unique_id(self) -> str:
"""Return the entity unique id."""
if self.sub_unique_id is None:
return self._device.serial
return f"{self._device.serial}-{self.sub_unique_id}"

@property
def sub_unique_id(self) -> str | None:
"""Return the entity sub unique id."""
return None

@property
def device_info(self) -> DeviceInfo:
"""Return device info of the entity."""
return {
"identifiers": {(DOMAIN, self._device.serial)},
"name": self._name,
"manufacturer": "Dyson",
"model": self._device.device_type,
}
48 changes: 48 additions & 0 deletions homeassistant/components/dyson_local/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Binary sensor platform for dyson."""

from typing import Callable

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_BATTERY_CHARGING,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant

from . import DysonEntity
from .const import DATA_DEVICES, DOMAIN


async def async_setup_entry(
hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up Dyson binary sensor from a config entry."""
device = hass.data[DOMAIN][DATA_DEVICES][config_entry.entry_id]
name = config_entry.data[CONF_NAME]
entities = [DysonVacuumBatteryChargingSensor(device, name)]
async_add_entities(entities)


class DysonVacuumBatteryChargingSensor(DysonEntity, BinarySensorEntity):
"""Dyson vacuum battery charging sensor."""

@property
def is_on(self) -> bool:
"""Return if the sensor is on."""
return self._device.is_charging

@property
def device_class(self) -> str:
"""Return the device class of the sensor."""
return DEVICE_CLASS_BATTERY_CHARGING

@property
def sub_name(self) -> str:
"""Return the name of the sensor."""
return "Battery Charging"

@property
def sub_unique_id(self):
"""Return the sensor's unique id."""
return "battery_charging"
129 changes: 129 additions & 0 deletions homeassistant/components/dyson_local/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""Config flow for Dyson integration."""
from __future__ import annotations

import logging

from libdyson import (
DEVICE_TYPE_360_EYE,
DEVICE_TYPE_360_HEURIST,
DEVICE_TYPE_NAMES,
get_device,
get_mqtt_info_from_wifi_info,
)
from libdyson.exceptions import (
DysonException,
DysonFailedToParseWifiInfo,
DysonInvalidCredential,
)
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.exceptions import HomeAssistantError

from .const import CONF_CREDENTIAL, CONF_DEVICE_TYPE, CONF_SERIAL, DOMAIN

_LOGGER = logging.getLogger(__name__)

DISCOVERY_TIMEOUT = 10

CONF_METHOD = "method"
CONF_SSID = "ssid"
CONF_PASSWORD = "password"

SETUP_METHODS = {
"wifi": "Setup using WiFi information",
"manual": "Setup manually",
}

SUPPORTED_DEVICE_TYPES = [
DEVICE_TYPE_360_EYE,
DEVICE_TYPE_360_HEURIST,
]
SUPPORTED_DEVICE_TYPE_NAMES = {
device_type: name
for device_type, name in DEVICE_TYPE_NAMES.items()
if device_type in SUPPORTED_DEVICE_TYPES
}


class DysonLocalConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Dyson local config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

def __init__(self):
"""Initialize the config flow."""
self._device_info = None

async def async_step_user(self, user_input: dict | None = None):
"""Handle step to setup using device WiFi information."""
errors = {}
if user_input is not None:
try:
serial, credential, device_type = get_mqtt_info_from_wifi_info(
user_input[CONF_SSID], user_input[CONF_PASSWORD]
)
except DysonFailedToParseWifiInfo:
errors["base"] = "cannot_parse_wifi_info"
else:
for entry in self._async_current_entries():
if entry.unique_id == serial:
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()

device_type_name = DEVICE_TYPE_NAMES[device_type]
_LOGGER.debug("Successfully parse WiFi information")
_LOGGER.debug("Serial: %s", serial)
_LOGGER.debug("Device Type: %s", device_type)
_LOGGER.debug("Device Type Name: %s", device_type_name)

host = user_input[CONF_HOST]
device = get_device(serial, credential, device_type)
try:
await self.hass.async_add_executor_job(device.connect, host)
except DysonInvalidCredential:
errors["base"] = "invalid_auth"
except DysonException as err:
_LOGGER.debug("Failed to connect to device: %s", err)
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title=device_type_name,
data={
CONF_SERIAL: serial,
CONF_CREDENTIAL: credential,
CONF_DEVICE_TYPE: device_type,
CONF_NAME: device_type_name,
CONF_HOST: host,
},
)

user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_SSID, default=user_input.get(CONF_SSID, "")): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
}
),
errors=errors,
)


class CannotConnect(HomeAssistantError):
"""Represents connection failure."""


class CannotFind(HomeAssistantError):
"""Represents discovery failure."""


class InvalidAuth(HomeAssistantError):
"""Represents invalid authentication."""
9 changes: 9 additions & 0 deletions homeassistant/components/dyson_local/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants for Dyson Local."""

DOMAIN = "dyson_local"

CONF_SERIAL = "serial"
CONF_CREDENTIAL = "credential"
CONF_DEVICE_TYPE = "device_type"

DATA_DEVICES = "devices"
9 changes: 9 additions & 0 deletions homeassistant/components/dyson_local/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "dyson_local",
"name": "Dyson Local",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dyson_local",
"codeowners": ["@shenxn"],
"requirements": ["libdyson==0.8.8"],
"iot_class": "local_push"
}
Loading

0 comments on commit a84106e

Please sign in to comment.