Add HomematicIP Cloud Config Flow and Entries loading (#14861)
* Add HomematicIP Cloud to config flow
* Inititial trial for config_flow
* Integrations text files
* Load and write config_flow and init homematicip_cloud
* Split into dedicated files
* Ceanup of text messages
* Working config_flow
* Move imports inside a function
* Enable laoding even no accesspoints are defined
* Revert unnecassary changes in CONFIG_SCHEMA
* Better error handling
* fix flask8
* Migration to async for token generation
* A few fixes
* Simplify config_flow
* Bump version to 9.6 with renamed package
* Requirements file
* First fixes after review
* Implement async_step_import
* Cleanup for Config Flow
* First tests for homematicip_cloud setup
* Remove config_flow tests
* Really remove all things
* Fix comment
* Update picture
* Add support for async_setup_entry to switch and climate platform
* Update path of the config_flow picture
* Refactoring for better tesability
* Further tests implemented
* Move 3th party lib inside function
* Fix lint
* Update requirments_test_all.txt file
* UPdate of requirments_test_all.txt did not work
* Furder cleanup in websocket connection
* Remove a test for the hap
* Revert "Remove a test for the hap"
This reverts commit 968d58cba1
.
* First tests implemented for config_flow
* Fix lint
* Rework of client registration process
* Implemented tests for config_flow 100% coverage
* Cleanup
* Cleanup comments and code
* Try to fix import problem
* Add homematicip to the test env requirements
pull/15225/merge
parent
0f1bcfd63b
commit
9970965718
|
@ -9,8 +9,8 @@ import logging
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
|
@ -26,12 +26,15 @@ HMIP_OPEN = 'open'
|
|||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP binary sensor devices."""
|
||||
"""Set up the binary sensor devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP binary sensor from a config entry."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
|
|
|
@ -12,8 +12,8 @@ from homeassistant.components.climate import (
|
|||
STATE_AUTO, STATE_MANUAL)
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -30,12 +30,14 @@ HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()}
|
|||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP climate devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP climate from a config entry."""
|
||||
from homematicip.group import HeatingGroup
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.groups:
|
||||
if isinstance(device, HeatingGroup):
|
||||
|
|
|
@ -1,262 +0,0 @@
|
|||
"""
|
||||
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
|
||||
|
||||
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.discovery import async_load_platform
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.core import callback
|
||||
|
||||
REQUIREMENTS = ['homematicip==0.9.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'homematicip_cloud'
|
||||
|
||||
COMPONENTS = [
|
||||
'sensor',
|
||||
'binary_sensor',
|
||||
'switch',
|
||||
'light',
|
||||
'climate',
|
||||
]
|
||||
|
||||
CONF_NAME = 'name'
|
||||
CONF_ACCESSPOINT = 'accesspoint'
|
||||
CONF_AUTHTOKEN = 'authtoken'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
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)
|
||||
|
||||
HMIP_ACCESS_POINT = 'Access Point'
|
||||
HMIP_HUB = 'HmIP-HUB'
|
||||
|
||||
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'
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the HomematicIP component."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
accesspoints = config.get(DOMAIN, [])
|
||||
for conf in accesspoints:
|
||||
_websession = async_get_clientsession(hass)
|
||||
_hmip = HomematicipConnector(hass, conf, _websession)
|
||||
try:
|
||||
await _hmip.init()
|
||||
except HmipConnectionError:
|
||||
_LOGGER.error('Failed to connect to the HomematicIP server, %s.',
|
||||
conf.get(CONF_ACCESSPOINT))
|
||||
return False
|
||||
|
||||
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)
|
||||
|
||||
self.home.on_update(self.async_update)
|
||||
self._accesspoint_connected = True
|
||||
|
||||
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()
|
||||
|
||||
@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 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, 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:
|
||||
name = "{} {}".format(self._home.name, name)
|
||||
if self.post is not None:
|
||||
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
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "HomematicIP Cloud",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Pick HomematicIP Accesspoint",
|
||||
"data": {
|
||||
"hapid": "Accesspoint ID (SGTIN)",
|
||||
"pin": "Pin Code (optional)",
|
||||
"name": "Name (optional, used as name prefix for all devices)"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link Accesspoint",
|
||||
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"register_failed": "Failed to register, please try again.",
|
||||
"invalid_pin": "Invalid PIN, please try again.",
|
||||
"press_the_button": "Please press the blue button.",
|
||||
"timeout_button": "Blue button press timeout, please try again."
|
||||
},
|
||||
"abort": {
|
||||
"unknown": "Unknown error occurred.",
|
||||
"conection_aborted": "Could not connect to HMIP server",
|
||||
"already_configured": "Accesspoint is already configured"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
Support for HomematicIP components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
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, HomematicipAuth # noqa: F401
|
||||
from .device import HomematicipGenericDevice # noqa: F401
|
||||
|
||||
REQUIREMENTS = ['homematicip==0.9.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({
|
||||
vol.Optional(CONF_NAME, default=''): vol.Any(cv.string),
|
||||
vol.Required(CONF_ACCESSPOINT): cv.string,
|
||||
vol.Required(CONF_AUTHTOKEN): cv.string,
|
||||
})]),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the HomematicIP component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
accesspoints = config.get(DOMAIN, [])
|
||||
|
||||
for conf in accesspoints:
|
||||
if conf[CONF_ACCESSPOINT] not in configured_haps(hass):
|
||||
hass.async_add_job(hass.config_entries.flow.async_init(
|
||||
DOMAIN, source='import', data={
|
||||
HMIPC_HAPID: conf[CONF_ACCESSPOINT],
|
||||
HMIPC_AUTHTOKEN: conf[CONF_AUTHTOKEN],
|
||||
HMIPC_NAME: conf[CONF_NAME],
|
||||
}
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Set up an accsspoint from a config entry."""
|
||||
hap = HomematicipHAP(hass, entry)
|
||||
hapid = entry.data[HMIPC_HAPID].replace('-', '').upper()
|
||||
hass.data[DOMAIN][hapid] = hap
|
||||
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()
|
|
@ -0,0 +1,97 @@
|
|||
"""Config flow to configure HomematicIP Cloud."""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
DOMAIN as HMIPC_DOMAIN, _LOGGER,
|
||||
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME)
|
||||
from .hap import HomematicipAuth
|
||||
|
||||
|
||||
@callback
|
||||
def configured_haps(hass):
|
||||
"""Return a set of the configured accesspoints."""
|
||||
return set(entry.data[HMIPC_HAPID] for entry
|
||||
in hass.config_entries.async_entries(HMIPC_DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(HMIPC_DOMAIN)
|
||||
class HomematicipCloudFlowHandler(data_entry_flow.FlowHandler):
|
||||
"""Config flow HomematicIP Cloud."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize HomematicIP Cloud config flow."""
|
||||
self.auth = None
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle a flow start."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
user_input[HMIPC_HAPID] = \
|
||||
user_input[HMIPC_HAPID].replace('-', '').upper()
|
||||
if user_input[HMIPC_HAPID] in configured_haps(self.hass):
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
self.auth = HomematicipAuth(self.hass, user_input)
|
||||
connected = await self.auth.async_setup()
|
||||
if connected:
|
||||
_LOGGER.info("Connection established")
|
||||
return await self.async_step_link()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(HMIPC_HAPID): str,
|
||||
vol.Optional(HMIPC_PIN): str,
|
||||
vol.Optional(HMIPC_NAME): str,
|
||||
}),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with the HomematicIP Cloud accesspoint."""
|
||||
errors = {}
|
||||
|
||||
pressed = await self.auth.async_checkbutton()
|
||||
if pressed:
|
||||
authtoken = await self.auth.async_register()
|
||||
if authtoken:
|
||||
_LOGGER.info("Write config entry")
|
||||
return self.async_create_entry(
|
||||
title=self.auth.config.get(HMIPC_HAPID),
|
||||
data={
|
||||
HMIPC_HAPID: self.auth.config.get(HMIPC_HAPID),
|
||||
HMIPC_AUTHTOKEN: authtoken,
|
||||
HMIPC_NAME: self.auth.config.get(HMIPC_NAME)
|
||||
})
|
||||
return self.async_abort(reason='conection_aborted')
|
||||
else:
|
||||
errors['base'] = 'press_the_button'
|
||||
|
||||
return self.async_show_form(step_id='link', errors=errors)
|
||||
|
||||
async def async_step_import(self, import_info):
|
||||
"""Import a new bridge as a config entry."""
|
||||
hapid = import_info[HMIPC_HAPID]
|
||||
authtoken = import_info[HMIPC_AUTHTOKEN]
|
||||
name = import_info[HMIPC_NAME]
|
||||
|
||||
hapid = hapid.replace('-', '').upper()
|
||||
if hapid in configured_haps(self.hass):
|
||||
return self.async_abort(reason='already_configured')
|
||||
|
||||
_LOGGER.info('Imported authentication for %s', hapid)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=hapid,
|
||||
data={
|
||||
HMIPC_HAPID: hapid,
|
||||
HMIPC_AUTHTOKEN: authtoken,
|
||||
HMIPC_NAME: name
|
||||
}
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
"""Constants for the HomematicIP Cloud component."""
|
||||
import logging
|
||||
|
||||
_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'
|
||||
|
||||
HMIPC_NAME = 'name'
|
||||
HMIPC_HAPID = 'hapid'
|
||||
HMIPC_AUTHTOKEN = 'authtoken'
|
||||
HMIPC_PIN = 'pin'
|
|
@ -0,0 +1,71 @@
|
|||
"""GenericDevice for the HomematicIP Cloud component."""
|
||||
import logging
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_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 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
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
"""Errors for the HomematicIP component."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class HmipcException(HomeAssistantError):
|
||||
"""Base class for HomematicIP exceptions."""
|
||||
|
||||
|
||||
class HmipcConnectionError(HmipcException):
|
||||
"""Unable to connect to the HomematicIP cloud server."""
|
||||
|
||||
|
||||
class HmipcConnectionWait(HmipcException):
|
||||
"""Wait for registration to the HomematicIP cloud server."""
|
||||
|
||||
|
||||
class HmipcRegistrationFailed(HmipcException):
|
||||
"""Registration on HomematicIP cloud failed."""
|
||||
|
||||
|
||||
class HmipcPressButton(HmipcException):
|
||||
"""User needs to press the blue button."""
|
|
@ -0,0 +1,256 @@
|
|||
"""Accesspoint for the HomematicIP Cloud component."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import (
|
||||
HMIPC_HAPID, HMIPC_AUTHTOKEN, HMIPC_PIN, HMIPC_NAME,
|
||||
COMPONENTS)
|
||||
from .errors import HmipcConnectionError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HomematicipAuth(object):
|
||||
"""Manages HomematicIP client registration."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize HomematicIP Cloud client registration."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.auth = None
|
||||
|
||||
async def async_setup(self):
|
||||
"""Connect to HomematicIP for registration."""
|
||||
try:
|
||||
self.auth = await self.get_auth(
|
||||
self.hass,
|
||||
self.config.get(HMIPC_HAPID),
|
||||
self.config.get(HMIPC_PIN)
|
||||
)
|
||||
return True
|
||||
except HmipcConnectionError:
|
||||
return False
|
||||
|
||||
async def async_checkbutton(self):
|
||||
"""Check blue butten has been pressed."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
await self.auth.isRequestAcknowledged()
|
||||
return True
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
|
||||
async def async_register(self):
|
||||
"""Register client at HomematicIP."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
try:
|
||||
authtoken = await self.auth.requestAuthToken()
|
||||
await self.auth.confirmAuthToken(authtoken)
|
||||
return authtoken
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
|
||||
async def get_auth(self, hass, hapid, pin):
|
||||
"""Create a HomematicIP access point object."""
|
||||
from homematicip.aio.auth import AsyncAuth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
auth = AsyncAuth(hass.loop, async_get_clientsession(hass))
|
||||
print(auth)
|
||||
try:
|
||||
await auth.init(hapid)
|
||||
if pin:
|
||||
auth.pin = pin
|
||||
await auth.connectionRequest('HomeAssistant')
|
||||
except HmipConnectionError:
|
||||
return False
|
||||
return auth
|
||||
|
||||
|
||||
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
|
||||
self._retry_setup = None
|
||||
|
||||
async def async_setup(self, tries=0):
|
||||
"""Initialize connection."""
|
||||
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 HmipcConnectionError:
|
||||
retry_delay = 2 ** min(tries + 1, 6)
|
||||
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
|
||||
"Retrying in %d seconds.",
|
||||
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
|
||||
|
||||
async def retry_setup(_now):
|
||||
"""Retry setup."""
|
||||
if await self.async_setup(tries + 1):
|
||||
self.config_entry.state = config_entries.ENTRY_STATE_LOADED
|
||||
|
||||
self._retry_setup = self.hass.helpers.event.async_call_later(
|
||||
retry_delay, retry_setup)
|
||||
|
||||
return False
|
||||
|
||||
_LOGGER.info('Connected to HomematicIP with HAP %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
|
||||
|
||||
try:
|
||||
await self.home.get_current_state()
|
||||
except HmipConnectionError:
|
||||
return
|
||||
hmip_events = await self.home.enable_events()
|
||||
try:
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
return
|
||||
|
||||
async def async_connect(self):
|
||||
"""Start websocket connection."""
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
await self.home.get_current_state()
|
||||
hmip_events = await self.home.enable_events()
|
||||
tries = 0
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
pass
|
||||
|
||||
if self._ws_close_requested:
|
||||
break
|
||||
self._ws_close_requested = False
|
||||
|
||||
tries += 1
|
||||
retry_delay = 2 ** min(tries + 1, 6)
|
||||
_LOGGER.error("Error connecting to HomematicIP with HAP %s. "
|
||||
"Retrying in %d seconds.",
|
||||
self.config_entry.data.get(HMIPC_HAPID), retry_delay)
|
||||
try:
|
||||
self._retry_task = self.hass.async_add_job(asyncio.sleep(
|
||||
retry_delay, loop=self.hass.loop))
|
||||
await self._retry_task
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
|
||||
async def async_reset(self):
|
||||
"""Close the websocket connection."""
|
||||
self._ws_close_requested = True
|
||||
if self._retry_setup is not None:
|
||||
self._retry_setup.cancel()
|
||||
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 HomematicIP access point object."""
|
||||
from homematicip.aio.home import AsyncHome
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
|
||||
home = AsyncHome(hass.loop, async_get_clientsession(hass))
|
||||
|
||||
home.name = name
|
||||
home.label = 'Access Point'
|
||||
home.modelType = 'HmIP-HAP'
|
||||
|
||||
home.set_auth_token(authtoken)
|
||||
try:
|
||||
await home.init(hapid)
|
||||
await home.get_current_state()
|
||||
except HmipConnectionError:
|
||||
raise HmipcConnectionError
|
||||
home.on_update(self.async_update)
|
||||
hass.loop.create_task(self.async_connect())
|
||||
|
||||
return home
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "HomematicIP Cloud",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Pick HomematicIP Accesspoint",
|
||||
"data": {
|
||||
"hapid": "Accesspoint ID (SGTIN)",
|
||||
"pin": "Pin Code (optional)",
|
||||
"name": "Name (optional, used as name prefix for all devices)"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link Accesspoint",
|
||||
"description": "Press the blue button on the accesspoint and the submit button to register HomematicIP with Home Assistant.\n\n![Location of button on bridge](/static/images/config_flows/config_homematicip_cloud.png)"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"register_failed": "Failed to register, please try again.",
|
||||
"invalid_pin": "Invalid PIN, please try again.",
|
||||
"press_the_button": "Please press the blue button.",
|
||||
"timeout_button": "Blue button press timeout, please try again."
|
||||
},
|
||||
"abort": {
|
||||
"unknown": "Unknown error occurred.",
|
||||
"conection_aborted": "Could not connect to HMIP server",
|
||||
"already_configured": "Accesspoint is already configured"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,8 +9,8 @@ import logging
|
|||
|
||||
from homeassistant.components.light import Light
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
|
@ -23,13 +23,16 @@ ATTR_PROFILE_MODE = 'profile_mode'
|
|||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP light devices."""
|
||||
"""Old way of setting up HomematicIP lights."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP lights from a config entry."""
|
||||
from homematicip.device import (
|
||||
BrandSwitchMeasuring)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
|
|
|
@ -8,8 +8,8 @@ https://home-assistant.io/components/sensor.homematicip_cloud/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_ILLUMINANCE)
|
||||
|
@ -36,15 +36,17 @@ STATE_SABOTAGE = 'sabotage'
|
|||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP sensors devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP sensors from a config entry."""
|
||||
from homematicip.device import (
|
||||
HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
|
||||
TemperatureHumiditySensorDisplay, MotionDetectorIndoor)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = [HomematicipAccesspointStatus(home)]
|
||||
|
||||
for device in home.devices:
|
||||
if isinstance(device, HeatingThermostat):
|
||||
devices.append(HomematicipHeatingThermostat(home, device))
|
||||
|
|
|
@ -95,7 +95,7 @@ def toggle(hass, entity_id=None):
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for switches."""
|
||||
component = EntityComponent(
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES)
|
||||
await component.async_setup(config)
|
||||
|
||||
|
@ -132,6 +132,16 @@ async def async_setup(hass, config):
|
|||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config 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 SwitchDevice(ToggleEntity):
|
||||
"""Representation of a switch."""
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import logging
|
|||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
||||
ATTR_HOME_ID)
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
|
@ -24,13 +24,16 @@ ATTR_PROFILE_MODE = 'profile_mode'
|
|||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP switch devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP switch from a config entry."""
|
||||
from homematicip.device import (
|
||||
PlugableSwitch, PlugableSwitchMeasuring,
|
||||
BrandSwitchMeasuring)
|
||||
|
||||
if discovery_info is None:
|
||||
return
|
||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
|
|
|
@ -127,6 +127,7 @@ HANDLERS = Registry()
|
|||
FLOWS = [
|
||||
'cast',
|
||||
'deconz',
|
||||
'homematicip_cloud',
|
||||
'hue',
|
||||
'nest',
|
||||
'sonos',
|
||||
|
|
|
@ -421,7 +421,7 @@ home-assistant-frontend==20180704.0
|
|||
# homekit==0.6
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==0.9.4
|
||||
homematicip==0.9.6
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.10.3
|
||||
|
|
|
@ -83,6 +83,9 @@ holidays==0.9.5
|
|||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20180704.0
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==0.9.6
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
# homeassistant.components.sensor.influxdb
|
||||
influxdb==5.0.0
|
||||
|
|
|
@ -56,6 +56,7 @@ TEST_REQUIREMENTS = (
|
|||
'hbmqtt',
|
||||
'holidays',
|
||||
'home-assistant-frontend',
|
||||
'homematicip',
|
||||
'influxdb',
|
||||
'libpurecoollink',
|
||||
'libsoundtouch',
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the HomematicIP Cloud component."""
|
|
@ -0,0 +1,150 @@
|
|||
"""Tests for HomematicIP Cloud config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.homematicip_cloud import hap as hmipc
|
||||
from homeassistant.components.homematicip_cloud import config_flow, const
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
|
||||
|
||||
async def test_flow_works(hass):
|
||||
"""Test config flow works."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
hap = hmipc.HomematicipAuth(hass, config)
|
||||
with patch.object(hap, 'get_auth', return_value=mock_coro()), \
|
||||
patch.object(hmipc.HomematicipAuth, 'async_checkbutton',
|
||||
return_value=mock_coro(True)), \
|
||||
patch.object(hmipc.HomematicipAuth, 'async_register',
|
||||
return_value=mock_coro(True)):
|
||||
hap.authtoken = 'ABC'
|
||||
result = await flow.async_step_init(user_input=config)
|
||||
|
||||
assert hap.authtoken == 'ABC'
|
||||
assert result['type'] == 'create_entry'
|
||||
|
||||
|
||||
async def test_flow_init_connection_error(hass):
|
||||
"""Test config flow with accesspoint connection error."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch.object(hmipc.HomematicipAuth, 'async_setup',
|
||||
return_value=mock_coro(False)):
|
||||
result = await flow.async_step_init(user_input=config)
|
||||
assert result['type'] == 'form'
|
||||
|
||||
|
||||
async def test_flow_link_connection_error(hass):
|
||||
"""Test config flow client registration connection error."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch.object(hmipc.HomematicipAuth, 'async_setup',
|
||||
return_value=mock_coro(True)), \
|
||||
patch.object(hmipc.HomematicipAuth, 'async_checkbutton',
|
||||
return_value=mock_coro(True)), \
|
||||
patch.object(hmipc.HomematicipAuth, 'async_register',
|
||||
return_value=mock_coro(False)):
|
||||
result = await flow.async_step_init(user_input=config)
|
||||
assert result['type'] == 'abort'
|
||||
|
||||
|
||||
async def test_flow_link_press_button(hass):
|
||||
"""Test config flow ask for pressing the blue button."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch.object(hmipc.HomematicipAuth, 'async_setup',
|
||||
return_value=mock_coro(True)), \
|
||||
patch.object(hmipc.HomematicipAuth, 'async_checkbutton',
|
||||
return_value=mock_coro(False)):
|
||||
result = await flow.async_step_init(user_input=config)
|
||||
assert result['type'] == 'form'
|
||||
assert result['errors'] == {'base': 'press_the_button'}
|
||||
|
||||
|
||||
async def test_init_flow_show_form(hass):
|
||||
"""Test config flow shows up with a form."""
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_init(user_input=None)
|
||||
assert result['type'] == 'form'
|
||||
|
||||
|
||||
async def test_init_already_configured(hass):
|
||||
"""Test accesspoint is already configured."""
|
||||
MockConfigEntry(domain=const.DOMAIN, data={
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
}).add_to_hass(hass)
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_init(user_input=config)
|
||||
assert result['type'] == 'abort'
|
||||
|
||||
|
||||
async def test_import_config(hass):
|
||||
"""Test importing a host with an existing config file."""
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_import({
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip'
|
||||
})
|
||||
|
||||
assert result['type'] == 'create_entry'
|
||||
assert result['title'] == 'ABC123'
|
||||
assert result['data'] == {
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip'
|
||||
}
|
||||
|
||||
|
||||
async def test_import_existing_config(hass):
|
||||
"""Test abort of an existing accesspoint from config."""
|
||||
flow = config_flow.HomematicipCloudFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
MockConfigEntry(domain=const.DOMAIN, data={
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
}).add_to_hass(hass)
|
||||
|
||||
result = await flow.async_step_import({
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip'
|
||||
})
|
||||
|
||||
assert result['type'] == 'abort'
|
|
@ -0,0 +1,113 @@
|
|||
"""Test HomematicIP Cloud accesspoint."""
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components.homematicip_cloud import hap as hmipc
|
||||
from homeassistant.components.homematicip_cloud import const, errors
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
async def test_auth_setup(hass):
|
||||
"""Test auth setup for client registration."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipAuth(hass, config)
|
||||
with patch.object(hap, 'get_auth', return_value=mock_coro()):
|
||||
assert await hap.async_setup() is True
|
||||
|
||||
|
||||
async def test_auth_setup_connection_error(hass):
|
||||
"""Test auth setup connection error behaviour."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipAuth(hass, config)
|
||||
with patch.object(hap, 'get_auth',
|
||||
side_effect=errors.HmipcConnectionError):
|
||||
assert await hap.async_setup() is False
|
||||
|
||||
|
||||
async def test_auth_auth_check_and_register(hass):
|
||||
"""Test auth client registration."""
|
||||
config = {
|
||||
const.HMIPC_HAPID: 'ABC123',
|
||||
const.HMIPC_PIN: '123',
|
||||
const.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipAuth(hass, config)
|
||||
hap.auth = Mock()
|
||||
with patch.object(hap.auth, 'isRequestAcknowledged',
|
||||
return_value=mock_coro()), \
|
||||
patch.object(hap.auth, 'requestAuthToken',
|
||||
return_value=mock_coro('ABC')), \
|
||||
patch.object(hap.auth, 'confirmAuthToken',
|
||||
return_value=mock_coro()):
|
||||
assert await hap.async_checkbutton() is True
|
||||
assert await hap.async_register() == 'ABC'
|
||||
|
||||
|
||||
async def test_hap_setup_works(aioclient_mock):
|
||||
"""Test a successful setup of a accesspoint."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
home = Mock()
|
||||
entry.data = {
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipHAP(hass, entry)
|
||||
with patch.object(hap, 'get_hap', return_value=mock_coro(home)):
|
||||
assert await hap.async_setup() is True
|
||||
|
||||
assert hap.home is home
|
||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5
|
||||
assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \
|
||||
(entry, 'binary_sensor')
|
||||
|
||||
|
||||
async def test_hap_setup_connection_error():
|
||||
"""Test a failed accesspoint setup."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
entry.data = {
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipHAP(hass, entry)
|
||||
with patch.object(hap, 'get_hap',
|
||||
side_effect=errors.HmipcConnectionError):
|
||||
assert await hap.async_setup() is False
|
||||
|
||||
assert len(hass.async_add_job.mock_calls) == 0
|
||||
assert len(hass.config_entries.flow.async_init.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_hap_reset_unloads_entry_if_setup():
|
||||
"""Test calling reset while the entry has been setup."""
|
||||
hass = Mock()
|
||||
entry = Mock()
|
||||
home = Mock()
|
||||
entry.data = {
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
hap = hmipc.HomematicipHAP(hass, entry)
|
||||
with patch.object(hap, 'get_hap', return_value=mock_coro(home)):
|
||||
assert await hap.async_setup() is True
|
||||
|
||||
assert hap.home is home
|
||||
assert len(hass.services.async_register.mock_calls) == 0
|
||||
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 5
|
||||
|
||||
hass.config_entries.async_forward_entry_unload.return_value = \
|
||||
mock_coro(True)
|
||||
await hap.async_reset()
|
||||
|
||||
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 5
|
|
@ -0,0 +1,103 @@
|
|||
"""Test HomematicIP Cloud setup process."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import homematicip_cloud as hmipc
|
||||
|
||||
from tests.common import mock_coro, MockConfigEntry
|
||||
|
||||
|
||||
async def test_config_with_accesspoint_passed_to_config_entry(hass):
|
||||
"""Test that config for a accesspoint are loaded via config entry."""
|
||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||
patch.object(hmipc, 'configured_haps', return_value=[]):
|
||||
assert await async_setup_component(hass, hmipc.DOMAIN, {
|
||||
hmipc.DOMAIN: {
|
||||
hmipc.CONF_ACCESSPOINT: 'ABC123',
|
||||
hmipc.CONF_AUTHTOKEN: '123',
|
||||
hmipc.CONF_NAME: 'name',
|
||||
}
|
||||
}) is True
|
||||
|
||||
# Flow started for the access point
|
||||
assert len(mock_config_entries.flow.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_config_already_registered_not_passed_to_config_entry(hass):
|
||||
"""Test that an already registered accesspoint does not get imported."""
|
||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||
patch.object(hmipc, 'configured_haps', return_value=['ABC123']):
|
||||
assert await async_setup_component(hass, hmipc.DOMAIN, {
|
||||
hmipc.DOMAIN: {
|
||||
hmipc.CONF_ACCESSPOINT: 'ABC123',
|
||||
hmipc.CONF_AUTHTOKEN: '123',
|
||||
hmipc.CONF_NAME: 'name',
|
||||
}
|
||||
}) is True
|
||||
|
||||
# No flow started
|
||||
assert len(mock_config_entries.flow.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_setup_entry_successful(hass):
|
||||
"""Test setup entry is successful."""
|
||||
entry = MockConfigEntry(domain=hmipc.DOMAIN, data={
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
})
|
||||
entry.add_to_hass(hass)
|
||||
with patch.object(hmipc, 'HomematicipHAP') as mock_hap:
|
||||
mock_hap.return_value.async_setup.return_value = mock_coro(True)
|
||||
assert await async_setup_component(hass, hmipc.DOMAIN, {
|
||||
hmipc.DOMAIN: {
|
||||
hmipc.CONF_ACCESSPOINT: 'ABC123',
|
||||
hmipc.CONF_AUTHTOKEN: '123',
|
||||
hmipc.CONF_NAME: 'hmip',
|
||||
}
|
||||
}) is True
|
||||
|
||||
assert len(mock_hap.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_setup_defined_accesspoint(hass):
|
||||
"""Test we initiate config entry for the accesspoint."""
|
||||
with patch.object(hass, 'config_entries') as mock_config_entries, \
|
||||
patch.object(hmipc, 'configured_haps', return_value=[]):
|
||||
mock_config_entries.flow.async_init.return_value = mock_coro()
|
||||
assert await async_setup_component(hass, hmipc.DOMAIN, {
|
||||
hmipc.DOMAIN: {
|
||||
hmipc.CONF_ACCESSPOINT: 'ABC123',
|
||||
hmipc.CONF_AUTHTOKEN: '123',
|
||||
hmipc.CONF_NAME: 'hmip',
|
||||
}
|
||||
}) is True
|
||||
|
||||
assert len(mock_config_entries.flow.mock_calls) == 1
|
||||
assert mock_config_entries.flow.mock_calls[0][2]['data'] == {
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
}
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test being able to unload an entry."""
|
||||
entry = MockConfigEntry(domain=hmipc.DOMAIN, data={
|
||||
hmipc.HMIPC_HAPID: 'ABC123',
|
||||
hmipc.HMIPC_AUTHTOKEN: '123',
|
||||
hmipc.HMIPC_NAME: 'hmip',
|
||||
})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch.object(hmipc, 'HomematicipHAP') as mock_hap:
|
||||
mock_hap.return_value.async_setup.return_value = mock_coro(True)
|
||||
assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True
|
||||
|
||||
assert len(mock_hap.return_value.mock_calls) == 1
|
||||
|
||||
mock_hap.return_value.async_reset.return_value = mock_coro(True)
|
||||
assert await hmipc.async_unload_entry(hass, entry)
|
||||
assert len(mock_hap.return_value.async_reset.mock_calls) == 1
|
||||
assert hass.data[hmipc.DOMAIN] == {}
|
Loading…
Reference in New Issue