Dynalite Integration (#27841)
* Initial commit * ran hassfest and gen_requirements_all scripts * fixes per request from Paulus Schoutsen * ran gen_requirements_all * updated library version - removed some debug leftover * get_requirements again... * added documentation URL * ran isort * changed storage in hass.data[DOMAIN] to use entry_id instead of host * adopted unit tests to latest fix * Update const.py Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
b78d156f0e
commit
4467409e5c
15 changed files with 672 additions and 0 deletions
|
@ -83,6 +83,7 @@ homeassistant/components/discogs/* @thibmaek
|
||||||
homeassistant/components/doorbird/* @oblogic7
|
homeassistant/components/doorbird/* @oblogic7
|
||||||
homeassistant/components/dsmr_reader/* @depl0y
|
homeassistant/components/dsmr_reader/* @depl0y
|
||||||
homeassistant/components/dweet/* @fabaff
|
homeassistant/components/dweet/* @fabaff
|
||||||
|
homeassistant/components/dynalite/* @ziv1234
|
||||||
homeassistant/components/dyson/* @etheralm
|
homeassistant/components/dyson/* @etheralm
|
||||||
homeassistant/components/ecobee/* @marthoc
|
homeassistant/components/ecobee/* @marthoc
|
||||||
homeassistant/components/ecovacs/* @OverloadUT
|
homeassistant/components/ecovacs/* @OverloadUT
|
||||||
|
|
93
homeassistant/components/dynalite/__init__.py
Executable file
93
homeassistant/components/dynalite/__init__.py
Executable file
|
@ -0,0 +1,93 @@
|
||||||
|
"""Support for the Dynalite networks."""
|
||||||
|
from dynalite_devices_lib import BRIDGE_CONFIG_SCHEMA
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
|
# Loading the config flow file will register the flow
|
||||||
|
from .bridge import DynaliteBridge
|
||||||
|
from .config_flow import configured_hosts
|
||||||
|
from .const import CONF_BRIDGES, DATA_CONFIGS, DOMAIN, LOGGER
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_BRIDGES): vol.All(
|
||||||
|
cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass, config):
|
||||||
|
"""Set up the Dynalite platform."""
|
||||||
|
|
||||||
|
conf = config.get(DOMAIN)
|
||||||
|
LOGGER.debug("Setting up dynalite component config = %s", conf)
|
||||||
|
|
||||||
|
if conf is None:
|
||||||
|
conf = {}
|
||||||
|
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN][DATA_CONFIGS] = {}
|
||||||
|
|
||||||
|
configured = configured_hosts(hass)
|
||||||
|
|
||||||
|
# User has configured bridges
|
||||||
|
if CONF_BRIDGES not in conf:
|
||||||
|
return True
|
||||||
|
|
||||||
|
bridges = conf[CONF_BRIDGES]
|
||||||
|
|
||||||
|
for bridge_conf in bridges:
|
||||||
|
host = bridge_conf[CONF_HOST]
|
||||||
|
LOGGER.debug("async_setup host=%s conf=%s", host, bridge_conf)
|
||||||
|
|
||||||
|
# Store config in hass.data so the config entry can find it
|
||||||
|
hass.data[DOMAIN][DATA_CONFIGS][host] = bridge_conf
|
||||||
|
|
||||||
|
if host in configured:
|
||||||
|
LOGGER.debug("async_setup host=%s already configured", host)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={CONF_HOST: bridge_conf[CONF_HOST]},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a bridge from a config entry."""
|
||||||
|
LOGGER.debug("__init async_setup_entry %s", entry.data)
|
||||||
|
host = entry.data[CONF_HOST]
|
||||||
|
config = hass.data[DOMAIN][DATA_CONFIGS].get(host)
|
||||||
|
|
||||||
|
if config is None:
|
||||||
|
LOGGER.error("__init async_setup_entry empty config for host %s", host)
|
||||||
|
return False
|
||||||
|
|
||||||
|
bridge = DynaliteBridge(hass, entry)
|
||||||
|
|
||||||
|
if not await bridge.async_setup():
|
||||||
|
LOGGER.error("bridge.async_setup failed")
|
||||||
|
return False
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = bridge
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
LOGGER.error("async_unload_entry %s", entry.data)
|
||||||
|
bridge = hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
return await bridge.async_reset()
|
118
homeassistant/components/dynalite/bridge.py
Executable file
118
homeassistant/components/dynalite/bridge.py
Executable file
|
@ -0,0 +1,118 @@
|
||||||
|
"""Code to handle a Dynalite bridge."""
|
||||||
|
|
||||||
|
from dynalite_devices_lib import DynaliteDevices
|
||||||
|
from dynalite_lib import CONF_ALL
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import DATA_CONFIGS, DOMAIN, LOGGER
|
||||||
|
from .light import DynaliteLight
|
||||||
|
|
||||||
|
|
||||||
|
class BridgeError(Exception):
|
||||||
|
"""Class to throw exceptions from DynaliteBridge."""
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
"""Initialize the exception."""
|
||||||
|
super().__init__()
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class DynaliteBridge:
|
||||||
|
"""Manages a single Dynalite bridge."""
|
||||||
|
|
||||||
|
def __init__(self, hass, config_entry):
|
||||||
|
"""Initialize the system based on host parameter."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
self.hass = hass
|
||||||
|
self.area = {}
|
||||||
|
self.async_add_entities = None
|
||||||
|
self.waiting_entities = []
|
||||||
|
self.all_entities = {}
|
||||||
|
self.config = None
|
||||||
|
self.host = config_entry.data[CONF_HOST]
|
||||||
|
if self.host not in hass.data[DOMAIN][DATA_CONFIGS]:
|
||||||
|
LOGGER.info("invalid host - %s", self.host)
|
||||||
|
raise BridgeError(f"invalid host - {self.host}")
|
||||||
|
self.config = hass.data[DOMAIN][DATA_CONFIGS][self.host]
|
||||||
|
# Configure the dynalite devices
|
||||||
|
self.dynalite_devices = DynaliteDevices(
|
||||||
|
config=self.config,
|
||||||
|
newDeviceFunc=self.add_devices,
|
||||||
|
updateDeviceFunc=self.update_device,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_setup(self, tries=0):
|
||||||
|
"""Set up a Dynalite bridge."""
|
||||||
|
# Configure the dynalite devices
|
||||||
|
await self.dynalite_devices.async_setup()
|
||||||
|
|
||||||
|
self.hass.async_create_task(
|
||||||
|
self.hass.config_entries.async_forward_entry_setup(
|
||||||
|
self.config_entry, "light"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def add_devices(self, devices):
|
||||||
|
"""Call when devices should be added to home assistant."""
|
||||||
|
added_entities = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
if device.category == "light":
|
||||||
|
entity = DynaliteLight(device, self)
|
||||||
|
else:
|
||||||
|
LOGGER.debug("Illegal device category %s", device.category)
|
||||||
|
continue
|
||||||
|
added_entities.append(entity)
|
||||||
|
self.all_entities[entity.unique_id] = entity
|
||||||
|
|
||||||
|
if added_entities:
|
||||||
|
self.add_entities_when_registered(added_entities)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def update_device(self, device):
|
||||||
|
"""Call when a device or all devices should be updated."""
|
||||||
|
if device == CONF_ALL:
|
||||||
|
# This is used to signal connection or disconnection, so all devices may become available or not.
|
||||||
|
if self.dynalite_devices.available:
|
||||||
|
LOGGER.info("Connected to dynalite host")
|
||||||
|
else:
|
||||||
|
LOGGER.info("Disconnected from dynalite host")
|
||||||
|
for uid in self.all_entities:
|
||||||
|
self.all_entities[uid].try_schedule_ha()
|
||||||
|
else:
|
||||||
|
uid = device.unique_id
|
||||||
|
if uid in self.all_entities:
|
||||||
|
self.all_entities[uid].try_schedule_ha()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def register_add_entities(self, async_add_entities):
|
||||||
|
"""Add an async_add_entities for a category."""
|
||||||
|
self.async_add_entities = async_add_entities
|
||||||
|
if self.waiting_entities:
|
||||||
|
self.async_add_entities(self.waiting_entities)
|
||||||
|
|
||||||
|
def add_entities_when_registered(self, entities):
|
||||||
|
"""Add the entities to HA if async_add_entities was registered, otherwise queue until it is."""
|
||||||
|
if not entities:
|
||||||
|
return
|
||||||
|
if self.async_add_entities:
|
||||||
|
self.async_add_entities(entities)
|
||||||
|
else: # handle it later when it is registered
|
||||||
|
self.waiting_entities.extend(entities)
|
||||||
|
|
||||||
|
async def async_reset(self):
|
||||||
|
"""Reset this bridge to default state.
|
||||||
|
|
||||||
|
Will cancel any scheduled setup retry and will unload
|
||||||
|
the config entry.
|
||||||
|
"""
|
||||||
|
result = await self.hass.config_entries.async_forward_entry_unload(
|
||||||
|
self.config_entry, "light"
|
||||||
|
)
|
||||||
|
# None and True are OK
|
||||||
|
return result
|
58
homeassistant/components/dynalite/config_flow.py
Executable file
58
homeassistant/components/dynalite/config_flow.py
Executable file
|
@ -0,0 +1,58 @@
|
||||||
|
"""Config flow to configure Dynalite hub."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def configured_hosts(hass):
|
||||||
|
"""Return a set of the configured hosts."""
|
||||||
|
return set(
|
||||||
|
entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a Dynalite config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the Dynalite flow."""
|
||||||
|
self.host = None
|
||||||
|
|
||||||
|
async def async_step_import(self, import_info):
|
||||||
|
"""Import a new bridge as a config entry."""
|
||||||
|
LOGGER.debug("async_step_import - %s", import_info)
|
||||||
|
host = self.context[CONF_HOST] = import_info[CONF_HOST]
|
||||||
|
return await self._entry_from_bridge(host)
|
||||||
|
|
||||||
|
async def _entry_from_bridge(self, host):
|
||||||
|
"""Return a config entry from an initialized bridge."""
|
||||||
|
LOGGER.debug("entry_from_bridge - %s", host)
|
||||||
|
# Remove all other entries of hubs with same ID or host
|
||||||
|
|
||||||
|
same_hub_entries = [
|
||||||
|
entry.entry_id
|
||||||
|
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if entry.data[CONF_HOST] == host
|
||||||
|
]
|
||||||
|
|
||||||
|
LOGGER.debug("entry_from_bridge same_hub - %s", same_hub_entries)
|
||||||
|
|
||||||
|
if same_hub_entries:
|
||||||
|
await asyncio.wait(
|
||||||
|
[
|
||||||
|
self.hass.config_entries.async_remove(entry_id)
|
||||||
|
for entry_id in same_hub_entries
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(title=host, data={CONF_HOST: host})
|
11
homeassistant/components/dynalite/const.py
Executable file
11
homeassistant/components/dynalite/const.py
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
"""Constants for the Dynalite component."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
DOMAIN = "dynalite"
|
||||||
|
DATA_CONFIGS = "dynalite_configs"
|
||||||
|
|
||||||
|
CONF_BRIDGES = "bridges"
|
||||||
|
|
||||||
|
DEFAULT_NAME = "dynalite"
|
||||||
|
DEFAULT_PORT = 12345
|
84
homeassistant/components/dynalite/light.py
Executable file
84
homeassistant/components/dynalite/light.py
Executable file
|
@ -0,0 +1,84 @@
|
||||||
|
"""Support for Dynalite channels as lights."""
|
||||||
|
from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Record the async_add_entities function to add them later when received from Dynalite."""
|
||||||
|
LOGGER.debug("async_setup_entry light entry = %s", config_entry.data)
|
||||||
|
bridge = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
bridge.register_add_entities(async_add_entities)
|
||||||
|
|
||||||
|
|
||||||
|
class DynaliteLight(Light):
|
||||||
|
"""Representation of a Dynalite Channel as a Home Assistant Light."""
|
||||||
|
|
||||||
|
def __init__(self, device, bridge):
|
||||||
|
"""Initialize the base class."""
|
||||||
|
self._device = device
|
||||||
|
self._bridge = bridge
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device(self):
|
||||||
|
"""Return the underlying device - mostly for testing."""
|
||||||
|
return self._device
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the entity."""
|
||||||
|
return self._device.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return the unique ID of the entity."""
|
||||||
|
return self._device.unique_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return self._device.available
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hidden(self):
|
||||||
|
"""Return true if this entity should be hidden from UI."""
|
||||||
|
return self._device.hidden
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the entity."""
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Device info for this entity."""
|
||||||
|
return self._device.device_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
"""Return the brightness of this light between 0..255."""
|
||||||
|
return self._device.brightness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._device.is_on
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
"""Turn the light on."""
|
||||||
|
await self._device.async_turn_on(**kwargs)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
"""Turn the light off."""
|
||||||
|
await self._device.async_turn_off(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return SUPPORT_BRIGHTNESS
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def try_schedule_ha(self):
|
||||||
|
"""Schedule update HA state if configured."""
|
||||||
|
if self.hass:
|
||||||
|
self.schedule_update_ha_state()
|
9
homeassistant/components/dynalite/manifest.json
Executable file
9
homeassistant/components/dynalite/manifest.json
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"domain": "dynalite",
|
||||||
|
"name": "Philips Dynalite",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/dynalite",
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": ["@ziv1234"],
|
||||||
|
"requirements": ["dynalite_devices==0.1.17"]
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ FLOWS = [
|
||||||
"daikin",
|
"daikin",
|
||||||
"deconz",
|
"deconz",
|
||||||
"dialogflow",
|
"dialogflow",
|
||||||
|
"dynalite",
|
||||||
"ecobee",
|
"ecobee",
|
||||||
"elgato",
|
"elgato",
|
||||||
"emulated_roku",
|
"emulated_roku",
|
||||||
|
|
|
@ -455,6 +455,9 @@ dsmr_parser==0.18
|
||||||
# homeassistant.components.dweet
|
# homeassistant.components.dweet
|
||||||
dweepy==0.3.0
|
dweepy==0.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.dynalite
|
||||||
|
dynalite_devices==0.1.17
|
||||||
|
|
||||||
# homeassistant.components.rainforest_eagle
|
# homeassistant.components.rainforest_eagle
|
||||||
eagle200_reader==0.2.1
|
eagle200_reader==0.2.1
|
||||||
|
|
||||||
|
|
|
@ -167,6 +167,9 @@ distro==1.4.0
|
||||||
# homeassistant.components.dsmr
|
# homeassistant.components.dsmr
|
||||||
dsmr_parser==0.18
|
dsmr_parser==0.18
|
||||||
|
|
||||||
|
# homeassistant.components.dynalite
|
||||||
|
dynalite_devices==0.1.17
|
||||||
|
|
||||||
# homeassistant.components.ee_brightbox
|
# homeassistant.components.ee_brightbox
|
||||||
eebrightbox==0.0.4
|
eebrightbox==0.0.4
|
||||||
|
|
||||||
|
|
1
tests/components/dynalite/__init__.py
Executable file
1
tests/components/dynalite/__init__.py
Executable file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Dynalite component."""
|
136
tests/components/dynalite/test_bridge.py
Executable file
136
tests/components/dynalite/test_bridge.py
Executable file
|
@ -0,0 +1,136 @@
|
||||||
|
"""Test Dynalite bridge."""
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
|
from dynalite_lib import CONF_ALL
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN
|
||||||
|
from homeassistant.components.dynalite.bridge import BridgeError, DynaliteBridge
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bridge_setup():
|
||||||
|
"""Test a successful setup."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}}
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
dyn_bridge.dynalite_devices, "async_setup", return_value=mock_coro(True)
|
||||||
|
):
|
||||||
|
assert await dyn_bridge.async_setup() is True
|
||||||
|
|
||||||
|
forward_entries = set(
|
||||||
|
c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls
|
||||||
|
)
|
||||||
|
hass.config_entries.async_forward_entry_setup.assert_called_once()
|
||||||
|
assert forward_entries == set(["light"])
|
||||||
|
|
||||||
|
|
||||||
|
async def test_invalid_host():
|
||||||
|
"""Test without host in hass.data."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {}}}
|
||||||
|
|
||||||
|
dyn_bridge = None
|
||||||
|
with pytest.raises(BridgeError):
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
assert dyn_bridge is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_devices_then_register():
|
||||||
|
"""Test that add_devices work."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}}
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
|
||||||
|
device1 = Mock()
|
||||||
|
device1.category = "light"
|
||||||
|
device2 = Mock()
|
||||||
|
device2.category = "switch"
|
||||||
|
dyn_bridge.add_devices([device1, device2])
|
||||||
|
reg_func = Mock()
|
||||||
|
dyn_bridge.register_add_entities(reg_func)
|
||||||
|
reg_func.assert_called_once()
|
||||||
|
assert reg_func.mock_calls[0][1][0][0].device is device1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_register_then_add_devices():
|
||||||
|
"""Test that add_devices work after register_add_entities."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}}
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
|
||||||
|
device1 = Mock()
|
||||||
|
device1.category = "light"
|
||||||
|
device2 = Mock()
|
||||||
|
device2.category = "switch"
|
||||||
|
reg_func = Mock()
|
||||||
|
dyn_bridge.register_add_entities(reg_func)
|
||||||
|
dyn_bridge.add_devices([device1, device2])
|
||||||
|
reg_func.assert_called_once()
|
||||||
|
assert reg_func.mock_calls[0][1][0][0].device is device1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_device():
|
||||||
|
"""Test the update_device callback."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}}
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
with patch.object(dyn_bridge, "dynalite_devices") as devices_mock:
|
||||||
|
# Single device update
|
||||||
|
device1 = Mock()
|
||||||
|
device1.unique_id = "testing1"
|
||||||
|
device2 = Mock()
|
||||||
|
device2.unique_id = "testing2"
|
||||||
|
dyn_bridge.all_entities = {
|
||||||
|
device1.unique_id: device1,
|
||||||
|
device2.unique_id: device2,
|
||||||
|
}
|
||||||
|
dyn_bridge.update_device(device1)
|
||||||
|
device1.try_schedule_ha.assert_called_once()
|
||||||
|
device2.try_schedule_ha.assert_not_called()
|
||||||
|
# connected to network - all devices update
|
||||||
|
devices_mock.available = True
|
||||||
|
dyn_bridge.update_device(CONF_ALL)
|
||||||
|
assert device1.try_schedule_ha.call_count == 2
|
||||||
|
device2.try_schedule_ha.assert_called_once()
|
||||||
|
# disconnected from network - all devices update
|
||||||
|
devices_mock.available = False
|
||||||
|
dyn_bridge.update_device(CONF_ALL)
|
||||||
|
assert device1.try_schedule_ha.call_count == 3
|
||||||
|
assert device2.try_schedule_ha.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_reset():
|
||||||
|
"""Test async_reset."""
|
||||||
|
hass = Mock()
|
||||||
|
hass.config_entries.async_forward_entry_unload = Mock(
|
||||||
|
return_value=mock_coro(Mock())
|
||||||
|
)
|
||||||
|
entry = Mock()
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {DOMAIN: {DATA_CONFIGS: {host: {}}}}
|
||||||
|
dyn_bridge = DynaliteBridge(hass, entry)
|
||||||
|
await dyn_bridge.async_reset()
|
||||||
|
hass.config_entries.async_forward_entry_unload.assert_called_once()
|
||||||
|
assert hass.config_entries.async_forward_entry_unload.mock_calls[0] == call(
|
||||||
|
entry, "light"
|
||||||
|
)
|
36
tests/components/dynalite/test_config_flow.py
Executable file
36
tests/components/dynalite/test_config_flow.py
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
"""Test Dynalite config flow."""
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
|
from homeassistant.components.dynalite import config_flow
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_step_import():
|
||||||
|
"""Test a successful setup."""
|
||||||
|
flow_handler = config_flow.DynaliteFlowHandler()
|
||||||
|
with patch.object(flow_handler, "context", create=True):
|
||||||
|
with patch.object(flow_handler, "hass", create=True) as mock_hass:
|
||||||
|
with patch.object(
|
||||||
|
flow_handler, "async_create_entry", create=True
|
||||||
|
) as mock_create:
|
||||||
|
host = "1.2.3.4"
|
||||||
|
entry1 = Mock()
|
||||||
|
entry1.data = {"host": host}
|
||||||
|
entry2 = Mock()
|
||||||
|
entry2.data = {"host": "5.5"}
|
||||||
|
mock_hass.config_entries.async_entries = Mock(
|
||||||
|
return_value=[entry1, entry2]
|
||||||
|
)
|
||||||
|
mock_hass.config_entries.async_remove = Mock(
|
||||||
|
return_value=mock_coro(Mock())
|
||||||
|
)
|
||||||
|
await flow_handler.async_step_import({"host": "1.2.3.4"})
|
||||||
|
mock_hass.config_entries.async_remove.assert_called_once()
|
||||||
|
assert mock_hass.config_entries.async_remove.mock_calls[0] == call(
|
||||||
|
entry1.entry_id
|
||||||
|
)
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
assert mock_create.mock_calls[0] == call(
|
||||||
|
title=host, data={"host": host}
|
||||||
|
)
|
74
tests/components/dynalite/test_init.py
Executable file
74
tests/components/dynalite/test_init.py
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
"""Test Dynalite __init__."""
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
|
from homeassistant.components.dynalite import DATA_CONFIGS, DOMAIN, LOGGER
|
||||||
|
from homeassistant.components.dynalite.__init__ import (
|
||||||
|
async_setup,
|
||||||
|
async_setup_entry,
|
||||||
|
async_unload_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup():
|
||||||
|
"""Test a successful setup."""
|
||||||
|
new_host = "1.2.3.4"
|
||||||
|
old_host = "5.6.7.8"
|
||||||
|
hass = Mock()
|
||||||
|
hass.data = {}
|
||||||
|
config = {DOMAIN: {"bridges": [{"host": old_host}, {"host": new_host}]}}
|
||||||
|
mock_conf_host = Mock(return_value=[old_host])
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.dynalite.__init__.configured_hosts", mock_conf_host
|
||||||
|
):
|
||||||
|
await async_setup(hass, config)
|
||||||
|
mock_conf_host.assert_called_once()
|
||||||
|
assert mock_conf_host.mock_calls[0] == call(hass)
|
||||||
|
assert hass.data[DOMAIN][DATA_CONFIGS] == {
|
||||||
|
new_host: {"host": new_host},
|
||||||
|
old_host: {"host": old_host},
|
||||||
|
}
|
||||||
|
hass.async_create_task.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_entry():
|
||||||
|
"""Test setup of an entry."""
|
||||||
|
|
||||||
|
def async_mock(mock):
|
||||||
|
"""Return the return value of a mock from async."""
|
||||||
|
|
||||||
|
async def async_func(*args, **kwargs):
|
||||||
|
return mock()
|
||||||
|
|
||||||
|
return async_func
|
||||||
|
|
||||||
|
host = "1.2.3.4"
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
entry.data = {"host": host}
|
||||||
|
hass.data = {}
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN][DATA_CONFIGS] = {host: {}}
|
||||||
|
mock_async_setup = Mock(return_value=True)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.dynalite.__init__.DynaliteBridge.async_setup",
|
||||||
|
async_mock(mock_async_setup),
|
||||||
|
):
|
||||||
|
assert await async_setup_entry(hass, entry)
|
||||||
|
mock_async_setup.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_unload_entry():
|
||||||
|
"""Test unloading of an entry."""
|
||||||
|
hass = Mock()
|
||||||
|
mock_bridge = Mock()
|
||||||
|
mock_bridge.async_reset.return_value = mock_coro(True)
|
||||||
|
entry = Mock()
|
||||||
|
hass.data = {}
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = mock_bridge
|
||||||
|
await async_unload_entry(hass, entry)
|
||||||
|
LOGGER.error("XXX calls=%s", mock_bridge.mock_calls)
|
||||||
|
mock_bridge.async_reset.assert_called_once()
|
||||||
|
assert mock_bridge.mock_calls[0] == call.async_reset()
|
44
tests/components/dynalite/test_light.py
Executable file
44
tests/components/dynalite/test_light.py
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
"""Test Dynalite light."""
|
||||||
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
|
from homeassistant.components.dynalite import DOMAIN
|
||||||
|
from homeassistant.components.dynalite.light import DynaliteLight, async_setup_entry
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_setup():
|
||||||
|
"""Test a successful setup."""
|
||||||
|
hass = Mock()
|
||||||
|
entry = Mock()
|
||||||
|
async_add = Mock()
|
||||||
|
bridge = Mock()
|
||||||
|
hass.data = {DOMAIN: {entry.entry_id: bridge}}
|
||||||
|
await async_setup_entry(hass, entry, async_add)
|
||||||
|
bridge.register_add_entities.assert_called_once()
|
||||||
|
assert bridge.register_add_entities.mock_calls[0] == call(async_add)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light():
|
||||||
|
"""Test the light entity."""
|
||||||
|
device = Mock()
|
||||||
|
device.async_turn_on = Mock(return_value=mock_coro(Mock()))
|
||||||
|
device.async_turn_off = Mock(return_value=mock_coro(Mock()))
|
||||||
|
bridge = Mock()
|
||||||
|
dyn_light = DynaliteLight(device, bridge)
|
||||||
|
assert dyn_light.name is device.name
|
||||||
|
assert dyn_light.unique_id is device.unique_id
|
||||||
|
assert dyn_light.available is device.available
|
||||||
|
assert dyn_light.hidden is device.hidden
|
||||||
|
await dyn_light.async_update() # does nothing
|
||||||
|
assert dyn_light.device_info is device.device_info
|
||||||
|
assert dyn_light.brightness is device.brightness
|
||||||
|
assert dyn_light.is_on is device.is_on
|
||||||
|
await dyn_light.async_turn_on(aaa="bbb")
|
||||||
|
assert device.async_turn_on.mock_calls[0] == call(aaa="bbb")
|
||||||
|
await dyn_light.async_turn_off(ccc="ddd")
|
||||||
|
assert device.async_turn_off.mock_calls[0] == call(ccc="ddd")
|
||||||
|
with patch.object(dyn_light, "hass"):
|
||||||
|
with patch.object(dyn_light, "schedule_update_ha_state") as update_ha:
|
||||||
|
dyn_light.try_schedule_ha()
|
||||||
|
update_ha.assert_called_once()
|
Loading…
Add table
Add a link
Reference in a new issue