Refactoring for better tesability

This commit is contained in:
Mattias Welponer
2018-06-23 00:13:19 +02:00
parent 893e6394ce
commit e1ebc31cf4
3 changed files with 248 additions and 222 deletions

View File

@@ -5,22 +5,18 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback
from .const import (
DOMAIN, HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
CONF_ACCESSPOINT, CONF_AUTHTOKEN, CONF_NAME)
# Loading the config flow file will register the flow
from .config_flow import configured_haps
from .hap import HomematicipHAP, HomematicipGenericDevice # noqa: F401
REQUIREMENTS = ['homematicip==0.9.6']
@@ -34,30 +30,6 @@ CONFIG_SCHEMA = vol.Schema({
})]),
}, extra=vol.ALLOW_EXTRA)
ATTR_HOME_ID = 'home_id'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_GROUP_TYPE = 'group_type'
ATTR_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected'
ATTR_SABOTAGE = 'sabotage'
ATTR_OPERATION_LOCK = 'operation_lock'
COMPONENTS = [
'binary_sensor',
'climate',
'light',
'sensor',
'switch',
]
async def async_setup(hass, config):
"""Set up the HomematicIP component."""
@@ -79,205 +51,14 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry):
"""Set up a bridge from a config entry."""
"""Set up a hap from a config entry."""
hap = HomematicipHAP(hass, entry)
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
hass.data[DOMAIN][hapid] = hap
_LOGGER.info('Connected to HomematicIP server, %s.', hapid)
return await hap.async_init()
return await hap.async_setup()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID])
return await hap.async_reset()
class HomematicipHAP(object):
"""Manages HomematicIP http and websocket connection."""
def __init__(self, hass, config_entry):
"""Initialize HomematicIP cloud connection."""
from homematicip.aio.home import AsyncHome
self._hass = hass
self._ws_close_requested = False
self._retry_task = None
self._tries = 0
self.config_entry = config_entry
self._hapid = config_entry.data.get(HMIPC_HAPID)
self.home = AsyncHome(hass.loop, async_get_clientsession(hass))
self.home.set_auth_token(config_entry.data.get(HMIPC_AUTHTOKEN))
self.home.on_update(self.async_update)
self.home.name = config_entry.data.get(HMIPC_NAME)
self.home.label = 'Access Point'
self.home.modelType = 'HmIP-HAP'
self._accesspoint_connected = True
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, self.async_close()
)
async def async_init(self):
"""Initialize connection."""
from homematicip.base.base_connection import HmipConnectionError
try:
await self.home.init(self._hapid)
await self.home.get_current_state()
self._hass.loop.create_task(self.async_connect())
except HmipConnectionError:
_LOGGER.error('Failed to connect to the sever, %s.', self._hapid)
return False
for component in COMPONENTS:
self._hass.async_add_job(
self._hass.config_entries.async_forward_entry_setup(
self.config_entry, component)
)
return True
@callback
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
There are several occasions for this event to happen.
We are only interested to check whether the access point
is still connected. If not, device state changes cannot
be forwarded to hass. So if access point is disconnected all devices
are set to unavailable.
"""
if not self.home.connected:
_LOGGER.error(
"HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Explicitly getting an update as device states might have
# changed during access point disconnect."""
job = self._hass.async_add_job(self.get_state())
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
await self.home.get_current_state()
self.update_all()
def get_state_finished(self, future):
"""Execute when get_state coroutine has finished."""
from homematicip.base.base_connection import HmipConnectionError
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
self._hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self):
"""Set all devices to unavailable and tell Hass."""
for device in self.home.devices:
device.unreach = True
self.update_all()
def update_all(self):
"""Signal all devices to update their state."""
for device in self.home.devices:
device.fire_update_event()
async def _handle_connection(self):
"""Handle websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
await self.home.get_current_state()
hmip_events = await self.home.enable_events()
try:
await hmip_events
except HmipConnectionError:
return
async def async_connect(self):
"""Start websocket connection."""
self._tries = 0
while True:
await self._handle_connection()
if self._ws_close_requested:
break
self._ws_close_requested = False
self._tries += 1
try:
self._retry_task = self._hass.async_add_job(asyncio.sleep(
2 ** min(9, self._tries), loop=self._hass.loop))
await self._retry_task
except asyncio.CancelledError:
break
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
self._tries)
async def async_close(self):
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_task is not None:
self._retry_task.cancel()
await self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device, post=None):
"""Initialize the generic device."""
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
self._device.on_update(self._device_changed)
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the generic device."""
name = self._device.label
if (self._home.name is not None and self._home.name != ''):
name = "{} {}".format(self._home.name, name)
if (self.post is not None and self.post != ''):
name = "{} {}".format(name, self.post)
return name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def available(self):
"""Device available."""
return not self._device.unreach
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return {
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_MODEL_TYPE: self._device.modelType
}

View File

@@ -5,6 +5,14 @@ _LOGGER = logging.getLogger('homeassistant.components.homematicip_cloud')
DOMAIN = 'homematicip_cloud'
COMPONENTS = [
'binary_sensor',
'climate',
'light',
'sensor',
'switch',
]
CONF_NAME = 'name'
CONF_ACCESSPOINT = 'accesspoint'
CONF_AUTHTOKEN = 'authtoken'

View File

@@ -0,0 +1,237 @@
"""
Support for HomematicIP components.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematicip_cloud/
"""
import asyncio
import logging
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback
from .const import (
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_NAME,
COMPONENTS)
_LOGGER = logging.getLogger(__name__)
ATTR_HOME_ID = 'home_id'
ATTR_HOME_NAME = 'home_name'
ATTR_DEVICE_ID = 'device_id'
ATTR_DEVICE_LABEL = 'device_label'
ATTR_STATUS_UPDATE = 'status_update'
ATTR_FIRMWARE_STATE = 'firmware_state'
ATTR_UNREACHABLE = 'unreachable'
ATTR_LOW_BATTERY = 'low_battery'
ATTR_MODEL_TYPE = 'model_type'
ATTR_GROUP_TYPE = 'group_type'
ATTR_DEVICE_RSSI = 'device_rssi'
ATTR_DUTY_CYCLE = 'duty_cycle'
ATTR_CONNECTED = 'connected'
ATTR_SABOTAGE = 'sabotage'
ATTR_OPERATION_LOCK = 'operation_lock'
class HomematicipHAP(object):
"""Manages HomematicIP http and websocket connection."""
def __init__(self, hass, config_entry):
"""Initialize HomematicIP cloud connection."""
self._hass = hass
self.config_entry = config_entry
self.home = None
self._ws_close_requested = False
self._retry_task = None
self._tries = 0
self._accesspoint_connected = True
async def async_setup(self):
"""Initialize connection."""
from homematicip.base.base_connection import HmipConnectionError
try:
self.home = await self.get_hap(
self._hass,
self.config_entry.data.get(HMIPC_HAPID),
self.config_entry.data.get(HMIPC_AUTHTOKEN),
self.config_entry.data.get(HMIPC_NAME)
)
except HmipConnectionError:
_LOGGER.error('Failed to connect to the sever, %s.',
self.config_entry.data.get(HMIPC_HAPID))
return False
_LOGGER.info('Connected to HomematicIP server, %s.',
self.config_entry.data.get(HMIPC_HAPID))
for component in COMPONENTS:
self._hass.async_add_job(
self._hass.config_entries.async_forward_entry_setup(
self.config_entry, component)
)
return True
@callback
def async_update(self, *args, **kwargs):
"""Async update the home device.
Triggered when the hmip HOME_CHANGED event has fired.
There are several occasions for this event to happen.
We are only interested to check whether the access point
is still connected. If not, device state changes cannot
be forwarded to hass. So if access point is disconnected all devices
are set to unavailable.
"""
if not self.home.connected:
_LOGGER.error(
"HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False
self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Explicitly getting an update as device states might have
# changed during access point disconnect."""
job = self._hass.async_add_job(self.get_state())
job.add_done_callback(self.get_state_finished)
async def get_state(self):
"""Update hmip state and tell hass."""
await self.home.get_current_state()
self.update_all()
def get_state_finished(self, future):
"""Execute when get_state coroutine has finished."""
from homematicip.base.base_connection import HmipConnectionError
try:
future.result()
except HmipConnectionError:
# Somehow connection could not recover. Will disconnect and
# so reconnect loop is taking over.
_LOGGER.error(
"updating state after himp access point reconnect failed.")
self._hass.async_add_job(self.home.disable_events())
def set_all_to_unavailable(self):
"""Set all devices to unavailable and tell Hass."""
for device in self.home.devices:
device.unreach = True
self.update_all()
def update_all(self):
"""Signal all devices to update their state."""
for device in self.home.devices:
device.fire_update_event()
async def _handle_connection(self):
"""Handle websocket connection."""
from homematicip.base.base_connection import HmipConnectionError
await self.home.get_current_state()
hmip_events = await self.home.enable_events()
try:
await hmip_events
except HmipConnectionError:
return
async def async_connect(self):
"""Start websocket connection."""
self._tries = 0
while True:
await self._handle_connection()
if self._ws_close_requested:
break
self._ws_close_requested = False
self._tries += 1
try:
self._retry_task = self._hass.async_add_job(asyncio.sleep(
2 ** min(9, self._tries), loop=self._hass.loop))
await self._retry_task
except asyncio.CancelledError:
break
_LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.',
self._tries)
async def async_reset(self):
"""Close the websocket connection."""
self._ws_close_requested = True
if self._retry_task is not None:
self._retry_task.cancel()
self.home.disable_events()
_LOGGER.info("Closed connection to HomematicIP cloud server.")
for component in COMPONENTS:
await self._hass.config_entries.async_forward_entry_unload(
self.config_entry, component)
return True
async def get_hap(self, hass, hapid, authtoken, name):
"""Create a hap object."""
from homematicip.aio.home import AsyncHome
home = AsyncHome(hass.loop, async_get_clientsession(hass))
home.name = name
home.label = 'Access Point'
home.modelType = 'HmIP-HAP'
home.set_auth_token(authtoken)
await home.init(hapid)
await home.get_current_state()
home.on_update(self.async_update)
hass.loop.create_task(self.async_connect())
return home
class HomematicipGenericDevice(Entity):
"""Representation of an HomematicIP generic device."""
def __init__(self, home, device, post=None):
"""Initialize the generic device."""
self._home = home
self._device = device
self.post = post
_LOGGER.info('Setting up %s (%s)', self.name,
self._device.modelType)
async def async_added_to_hass(self):
"""Register callbacks."""
self._device.on_update(self._device_changed)
def _device_changed(self, json, **kwargs):
"""Handle device state changes."""
_LOGGER.debug('Event %s (%s)', self.name, self._device.modelType)
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the generic device."""
name = self._device.label
if (self._home.name is not None and self._home.name != ''):
name = "{} {}".format(self._home.name, name)
if (self.post is not None and self.post != ''):
name = "{} {}".format(name, self.post)
return name
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def available(self):
"""Device available."""
return not self._device.unreach
@property
def device_state_attributes(self):
"""Return the state attributes of the generic device."""
return {
ATTR_LOW_BATTERY: self._device.lowBat,
ATTR_MODEL_TYPE: self._device.modelType
}