deCONZ allow unloading of config entry (#14115)

* Working but incomplete

* Remove events on unload

* Add unload test

* Fix failing sensor test

* Improve unload test

* Move DeconzEvent to init

* Fix visual under-indentation
This commit is contained in:
Kane610 2018-04-29 16:16:20 +02:00 committed by Paulus Schoutsen
parent ef48a7ca2c
commit 3fd4987baf
8 changed files with 86 additions and 31 deletions

View file

@ -62,6 +62,11 @@ async def async_setup_entry(hass, entry):
return await hass.data[DOMAIN].async_setup_entry(entry) return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
# pylint: disable=no-self-use # pylint: disable=no-self-use
class BinarySensorDevice(Entity): class BinarySensorDevice(Entity):
"""Represent a binary sensor.""" """Represent a binary sensor."""

View file

@ -7,14 +7,17 @@ https://home-assistant.io/components/deconz/
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) CONF_API_KEY, CONF_EVENT, CONF_HOST,
from homeassistant.core import callback CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.util import slugify
from homeassistant.util.json import load_json from homeassistant.util.json import load_json
# Loading the config flow file will register the flow # Loading the config flow file will register the flow
from .config_flow import configured_hosts from .config_flow import configured_hosts
from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER from .const import (
CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==36'] REQUIREMENTS = ['pydeconz==36']
@ -26,6 +29,8 @@ CONFIG_SCHEMA = vol.Schema({
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_DECONZ = 'configure'
SERVICE_FIELD = 'field' SERVICE_FIELD = 'field'
SERVICE_ENTITY = 'entity' SERVICE_ENTITY = 'entity'
SERVICE_DATA = 'data' SERVICE_DATA = 'data'
@ -64,6 +69,7 @@ async def async_setup_entry(hass, config_entry):
Start websocket for push notification of state changes from deCONZ. Start websocket for push notification of state changes from deCONZ.
""" """
from pydeconz import DeconzSession from pydeconz import DeconzSession
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
if DOMAIN in hass.data: if DOMAIN in hass.data:
_LOGGER.error( _LOGGER.error(
"Config entry failed since one deCONZ instance already exists") "Config entry failed since one deCONZ instance already exists")
@ -82,6 +88,11 @@ async def async_setup_entry(hass, config_entry):
for component in ['binary_sensor', 'light', 'scene', 'sensor']: for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(hass.config_entries.async_forward_entry_setup( hass.async_add_job(hass.config_entries.async_forward_entry_setup(
config_entry, component)) config_entry, component))
hass.data[DATA_DECONZ_EVENT] = [DeconzEvent(
hass, sensor) for sensor in deconz.sensors.values()
if sensor.type in DECONZ_REMOTE]
deconz.start() deconz.start()
async def async_configure(call): async def async_configure(call):
@ -112,7 +123,7 @@ async def async_setup_entry(hass, config_entry):
return return
await deconz.async_put_state(field, data) await deconz.async_put_state(field, data)
hass.services.async_register( hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA)
@callback @callback
def deconz_shutdown(event): def deconz_shutdown(event):
@ -127,3 +138,39 @@ async def async_setup_entry(hass, config_entry):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown)
return True return True
async def async_unload_entry(hass, config_entry):
"""Unload deCONZ config entry."""
deconz = hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close()
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_ID] = []
return True
class DeconzEvent(object):
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View file

@ -5,4 +5,5 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz')
DOMAIN = 'deconz' DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf' CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_ID = 'deconz_entities'

View file

@ -95,6 +95,11 @@ async def async_setup_entry(hass, entry):
return await hass.data[DOMAIN].async_setup_entry(entry) return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class Scene(Entity): class Scene(Entity):
"""A scene is a group of entities and the states we want them to be.""" """A scene is a group of entities and the states we want them to be."""

View file

@ -41,3 +41,8 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, entry):
"""Setup a config entry.""" """Setup a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry) return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)

View file

@ -6,9 +6,8 @@ https://home-assistant.io/components/sensor.deconz/
""" """
from homeassistant.components.deconz import ( from homeassistant.components.deconz import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
from homeassistant.const import ( from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) from homeassistant.core import callback
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.helpers.icon import icon_for_battery_level
from homeassistant.util import slugify from homeassistant.util import slugify
@ -35,7 +34,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
for sensor in sensors.values(): for sensor in sensors.values():
if sensor and sensor.type in DECONZ_SENSOR: if sensor and sensor.type in DECONZ_SENSOR:
if sensor.type in DECONZ_REMOTE: if sensor.type in DECONZ_REMOTE:
DeconzEvent(hass, sensor)
if sensor.battery: if sensor.battery:
entities.append(DeconzBattery(sensor)) entities.append(DeconzBattery(sensor))
else: else:
@ -184,26 +182,3 @@ class DeconzBattery(Entity):
ATTR_EVENT_ID: slugify(self._device.name), ATTR_EVENT_ID: slugify(self._device.name),
} }
return attr return attr
class DeconzEvent(object):
"""When you want signals instead of entities.
Stateless sensors such as remotes are expected to generate an event
instead of a sensor entity in hass.
"""
def __init__(self, hass, device):
"""Register callback that will be used for signals."""
self._hass = hass
self._device = device
self._device.register_async_callback(self.async_update_callback)
self._event = 'deconz_{}'.format(CONF_EVENT)
self._id = slugify(self._device.name)
@callback
def async_update_callback(self, reason):
"""Fire the event if reason is that state is updated."""
if reason['state']:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View file

@ -107,3 +107,19 @@ async def test_setup_entry_successful(hass):
(entry, 'scene') (entry, 'scene')
assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \
(entry, 'sensor') (entry, 'sensor')
async def test_unload_entry(hass):
"""Test being able to unload an entry."""
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
with patch('pydeconz.DeconzSession.async_load_parameters',
return_value=mock_coro(True)):
assert await deconz.async_setup_entry(hass, entry) is True
assert deconz.DATA_DECONZ_EVENT in hass.data
hass.data[deconz.DATA_DECONZ_EVENT].append(Mock())
hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'}
assert await deconz.async_unload_entry(hass, entry)
assert deconz.DOMAIN not in hass.data
assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0
assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0

View file

@ -51,6 +51,7 @@ async def setup_bridge(hass, data):
return_value=mock_coro(data)): return_value=mock_coro(data)):
await bridge.async_load_parameters() await bridge.async_load_parameters()
hass.data[deconz.DOMAIN] = bridge hass.data[deconz.DOMAIN] = bridge
hass.data[deconz.DATA_DECONZ_EVENT] = []
hass.data[deconz.DATA_DECONZ_ID] = {} hass.data[deconz.DATA_DECONZ_ID] = {}
config_entry = config_entries.ConfigEntry( config_entry = config_entries.ConfigEntry(
1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test')