New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Re-implement HomematicIP cloud to async #13468
Merged
balloob
merged 15 commits into
home-assistant:dev
from
worm-ee:recode-homematicip_cloud-to-async
Apr 25, 2018
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bd9d104
Recode to async version of homematicip-rest-api
worm-ee ea2a1ac
Remove blank line
worm-ee 4b8fd19
Cleanup of access point status class
worm-ee 7e09231
Fix to loong line
worm-ee 8df9e54
Fix import errors
worm-ee d697637
Bugfix missing wait the _retry_task for sleep command
worm-ee bf448ab
Update comment
worm-ee 74bcd8c
Updates after review
worm-ee cbab26f
Small updates of logging and property name
worm-ee 637ef04
Fix DOMAIN and revert back to lowercase snakecase strings
worm-ee 5d37888
Fix intention and tripple double quotes
worm-ee 138c341
Fix travis build
worm-ee bb23d7c
Remove unnecessary state attributes
worm-ee 335aa0c
Fix optional name in configuration
worm-ee 5249110
Further reduction of state attributes
worm-ee File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,143 +5,181 @@ | |
https://home-assistant.io/components/homematicip_cloud/ | ||
""" | ||
|
||
import asyncio | ||
import logging | ||
from socket import timeout | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant.core import callback | ||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
import homeassistant.helpers.config_validation as cv | ||
from homeassistant.helpers.dispatcher import (dispatcher_send, | ||
async_dispatcher_connect) | ||
from homeassistant.helpers.discovery import load_platform | ||
from homeassistant.helpers.discovery import async_load_platform | ||
from homeassistant.helpers.entity import Entity | ||
|
||
REQUIREMENTS = ['homematicip==0.8'] | ||
REQUIREMENTS = ['homematicip==0.9.2.4'] | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
DOMAIN = 'homematicip_cloud' | ||
|
||
COMPONENTS = [ | ||
'sensor' | ||
] | ||
|
||
CONF_NAME = 'name' | ||
CONF_ACCESSPOINT = 'accesspoint' | ||
CONF_AUTHTOKEN = 'authtoken' | ||
|
||
CONFIG_SCHEMA = vol.Schema({ | ||
vol.Optional(DOMAIN): [vol.Schema({ | ||
vol.Optional(CONF_NAME, default=''): cv.string, | ||
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ | ||
vol.Optional(CONF_NAME): vol.Any(cv.string), | ||
vol.Required(CONF_ACCESSPOINT): cv.string, | ||
vol.Required(CONF_AUTHTOKEN): cv.string, | ||
})], | ||
})]), | ||
}, extra=vol.ALLOW_EXTRA) | ||
|
||
EVENT_HOME_CHANGED = 'homematicip_home_changed' | ||
EVENT_DEVICE_CHANGED = 'homematicip_device_changed' | ||
EVENT_GROUP_CHANGED = 'homematicip_group_changed' | ||
EVENT_SECURITY_CHANGED = 'homematicip_security_changed' | ||
EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' | ||
HMIP_ACCESS_POINT = 'Access Point' | ||
HMIP_HUB = 'HmIP-HUB' | ||
|
||
ATTR_HOME_ID = 'home_id' | ||
ATTR_HOME_LABEL = 'home_label' | ||
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_RSSI = 'rssi' | ||
ATTR_TYPE = 'type' | ||
ATTR_OPERATION_LOCK = 'operation_lock' | ||
|
||
|
||
def setup(hass, config): | ||
async def async_setup(hass, config): | ||
"""Set up the HomematicIP component.""" | ||
# pylint: disable=import-error, no-name-in-module | ||
from homematicip.home import Home | ||
from homematicip.base.base_connection import HmipConnectionError | ||
|
||
hass.data.setdefault(DOMAIN, {}) | ||
homes = hass.data[DOMAIN] | ||
accesspoints = config.get(DOMAIN, []) | ||
|
||
def _update_event(events): | ||
"""Handle incoming HomeMaticIP events.""" | ||
for event in events: | ||
etype = event['eventType'] | ||
edata = event['data'] | ||
if etype == 'DEVICE_CHANGED': | ||
dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) | ||
elif etype == 'GROUP_CHANGED': | ||
dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) | ||
elif etype == 'HOME_CHANGED': | ||
dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) | ||
elif etype == 'JOURNAL_CHANGED': | ||
dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) | ||
return True | ||
|
||
for device in accesspoints: | ||
name = device.get(CONF_NAME) | ||
accesspoint = device.get(CONF_ACCESSPOINT) | ||
authtoken = device.get(CONF_AUTHTOKEN) | ||
|
||
home = Home() | ||
if name.lower() == 'none': | ||
name = '' | ||
home.label = name | ||
for conf in accesspoints: | ||
_websession = async_get_clientsession(hass) | ||
_hmip = HomematicipConnector(hass, conf, _websession) | ||
try: | ||
home.set_auth_token(authtoken) | ||
home.init(accesspoint) | ||
if home.get_current_state(): | ||
_LOGGER.info("Connection to HMIP established") | ||
else: | ||
_LOGGER.warning("Connection to HMIP could not be established") | ||
return False | ||
except timeout: | ||
_LOGGER.warning("Connection to HMIP could not be established") | ||
await _hmip.init() | ||
except HmipConnectionError: | ||
_LOGGER.error('Failed to connect to the HomematicIP server, %s.', | ||
conf.get(CONF_ACCESSPOINT)) | ||
return False | ||
homes[home.id] = home | ||
home.onEvent += _update_event | ||
home.enable_events() | ||
_LOGGER.info('HUB name: %s, id: %s', home.label, home.id) | ||
|
||
for component in ['sensor']: | ||
load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) | ||
home = _hmip.home | ||
home.name = conf.get(CONF_NAME) | ||
home.label = HMIP_ACCESS_POINT | ||
home.modelType = HMIP_HUB | ||
|
||
hass.data[DOMAIN][home.id] = home | ||
_LOGGER.info('Connected to the HomematicIP server, %s.', | ||
conf.get(CONF_ACCESSPOINT)) | ||
homeid = {ATTR_HOME_ID: home.id} | ||
for component in COMPONENTS: | ||
hass.async_add_job(async_load_platform(hass, component, DOMAIN, | ||
homeid, config)) | ||
|
||
hass.loop.create_task(_hmip.connect()) | ||
return True | ||
|
||
|
||
class HomematicipConnector: | ||
"""Manages HomematicIP http and websocket connection.""" | ||
|
||
def __init__(self, hass, config, websession): | ||
"""Initialize HomematicIP cloud connection.""" | ||
from homematicip.async.home import AsyncHome | ||
self._hass = hass | ||
self._ws_close_requested = False | ||
self._retry_task = None | ||
self._tries = 0 | ||
self._accesspoint = config.get(CONF_ACCESSPOINT) | ||
_authtoken = config.get(CONF_AUTHTOKEN) | ||
|
||
self.home = AsyncHome(hass.loop, websession) | ||
self.home.set_auth_token(_authtoken) | ||
|
||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) | ||
|
||
async def init(self): | ||
"""Initialize connection.""" | ||
await self.home.init(self._accesspoint) | ||
await self.home.get_current_state() | ||
|
||
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 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 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): | ||
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.""" | ||
async_dispatcher_connect( | ||
self.hass, EVENT_DEVICE_CHANGED, self._device_changed) | ||
self._device.on_update(self._device_changed) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just add this to the init method. |
||
|
||
@callback | ||
def _device_changed(self, deviceid): | ||
def _device_changed(self, json, **kwargs): | ||
"""Handle device state changes.""" | ||
if deviceid is None or deviceid == self._device.id: | ||
_LOGGER.debug('Event device %s', self._device.label) | ||
self.async_schedule_update_ha_state() | ||
|
||
def _name(self, addon=''): | ||
"""Return the name of the device.""" | ||
name = '' | ||
if self._home.label != '': | ||
name += self._home.label + ' ' | ||
name += self._device.label | ||
if addon != '': | ||
name += ' ' + addon | ||
return name | ||
_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.""" | ||
return self._name() | ||
name = self._device.label | ||
if self._home.name is not None: | ||
name = "{} {}".format(self._home.name, name) | ||
if self.post is not None: | ||
name = "{} {}".format(name, self.post) | ||
return name | ||
|
||
@property | ||
def should_poll(self): | ||
|
@@ -153,24 +191,10 @@ def available(self): | |
"""Device available.""" | ||
return not self._device.unreach | ||
|
||
def _generic_state_attributes(self): | ||
@property | ||
def device_state_attributes(self): | ||
"""Return the state attributes of the generic device.""" | ||
laststatus = '' | ||
if self._device.lastStatusUpdate is not None: | ||
laststatus = self._device.lastStatusUpdate.isoformat() | ||
return { | ||
ATTR_HOME_LABEL: self._home.label, | ||
ATTR_DEVICE_LABEL: self._device.label, | ||
ATTR_HOME_ID: self._device.homeId, | ||
ATTR_DEVICE_ID: self._device.id.lower(), | ||
ATTR_STATUS_UPDATE: laststatus, | ||
ATTR_FIRMWARE_STATE: self._device.updateState.lower(), | ||
ATTR_LOW_BATTERY: self._device.lowBat, | ||
ATTR_RSSI: self._device.rssiDeviceValue, | ||
ATTR_TYPE: self._device.modelType | ||
ATTR_MODEL_TYPE: self._device.modelType | ||
} | ||
|
||
@property | ||
def device_state_attributes(self): | ||
"""Return the state attributes of the generic device.""" | ||
return self._generic_state_attributes() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to wait the _retry_task otherwise the sleep command just wanders off and does not make the loop wait.
await self._retry_task
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!