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
This commit is contained in:
parent
0f1bcfd63b
commit
9970965718
23 changed files with 1020 additions and 293 deletions
|
@ -9,8 +9,8 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||||
ATTR_HOME_ID)
|
HMIPC_HAPID)
|
||||||
|
|
||||||
DEPENDENCIES = ['homematicip_cloud']
|
DEPENDENCIES = ['homematicip_cloud']
|
||||||
|
|
||||||
|
@ -26,12 +26,15 @@ HMIP_OPEN = 'open'
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
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)
|
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||||
|
|
||||||
if discovery_info is None:
|
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||||
return
|
|
||||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
|
||||||
devices = []
|
devices = []
|
||||||
for device in home.devices:
|
for device in home.devices:
|
||||||
if isinstance(device, ShutterContact):
|
if isinstance(device, ShutterContact):
|
||||||
|
|
|
@ -12,8 +12,8 @@ from homeassistant.components.climate import (
|
||||||
STATE_AUTO, STATE_MANUAL)
|
STATE_AUTO, STATE_MANUAL)
|
||||||
from homeassistant.const import TEMP_CELSIUS
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||||
ATTR_HOME_ID)
|
HMIPC_HAPID)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the HomematicIP climate devices."""
|
"""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
|
from homematicip.group import HeatingGroup
|
||||||
|
|
||||||
if discovery_info is None:
|
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||||
return
|
|
||||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
|
||||||
|
|
||||||
devices = []
|
devices = []
|
||||||
for device in home.groups:
|
for device in home.groups:
|
||||||
if isinstance(device, HeatingGroup):
|
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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
homeassistant/components/homematicip_cloud/__init__.py
Normal file
65
homeassistant/components/homematicip_cloud/__init__.py
Normal file
|
@ -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()
|
97
homeassistant/components/homematicip_cloud/config_flow.py
Normal file
97
homeassistant/components/homematicip_cloud/config_flow.py
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
)
|
23
homeassistant/components/homematicip_cloud/const.py
Normal file
23
homeassistant/components/homematicip_cloud/const.py
Normal file
|
@ -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'
|
71
homeassistant/components/homematicip_cloud/device.py
Normal file
71
homeassistant/components/homematicip_cloud/device.py
Normal file
|
@ -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
|
||||||
|
}
|
22
homeassistant/components/homematicip_cloud/errors.py
Normal file
22
homeassistant/components/homematicip_cloud/errors.py
Normal file
|
@ -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."""
|
256
homeassistant/components/homematicip_cloud/hap.py
Normal file
256
homeassistant/components/homematicip_cloud/hap.py
Normal file
|
@ -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
|
30
homeassistant/components/homematicip_cloud/strings.json
Normal file
30
homeassistant/components/homematicip_cloud/strings.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.light import Light
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||||
ATTR_HOME_ID)
|
HMIPC_HAPID)
|
||||||
|
|
||||||
DEPENDENCIES = ['homematicip_cloud']
|
DEPENDENCIES = ['homematicip_cloud']
|
||||||
|
|
||||||
|
@ -23,13 +23,16 @@ ATTR_PROFILE_MODE = 'profile_mode'
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
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 (
|
from homematicip.device import (
|
||||||
BrandSwitchMeasuring)
|
BrandSwitchMeasuring)
|
||||||
|
|
||||||
if discovery_info is None:
|
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||||
return
|
|
||||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
|
||||||
devices = []
|
devices = []
|
||||||
for device in home.devices:
|
for device in home.devices:
|
||||||
if isinstance(device, BrandSwitchMeasuring):
|
if isinstance(device, BrandSwitchMeasuring):
|
||||||
|
|
|
@ -8,8 +8,8 @@ https://home-assistant.io/components/sensor.homematicip_cloud/
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||||
ATTR_HOME_ID)
|
HMIPC_HAPID)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY,
|
TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY,
|
||||||
DEVICE_CLASS_ILLUMINANCE)
|
DEVICE_CLASS_ILLUMINANCE)
|
||||||
|
@ -36,15 +36,17 @@ STATE_SABOTAGE = 'sabotage'
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the HomematicIP sensors devices."""
|
"""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 (
|
from homematicip.device import (
|
||||||
HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
|
HeatingThermostat, TemperatureHumiditySensorWithoutDisplay,
|
||||||
TemperatureHumiditySensorDisplay, MotionDetectorIndoor)
|
TemperatureHumiditySensorDisplay, MotionDetectorIndoor)
|
||||||
|
|
||||||
if discovery_info is None:
|
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||||
return
|
|
||||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
|
||||||
devices = [HomematicipAccesspointStatus(home)]
|
devices = [HomematicipAccesspointStatus(home)]
|
||||||
|
|
||||||
for device in home.devices:
|
for device in home.devices:
|
||||||
if isinstance(device, HeatingThermostat):
|
if isinstance(device, HeatingThermostat):
|
||||||
devices.append(HomematicipHeatingThermostat(home, device))
|
devices.append(HomematicipHeatingThermostat(home, device))
|
||||||
|
|
|
@ -95,7 +95,7 @@ def toggle(hass, entity_id=None):
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Track states and offer events for switches."""
|
"""Track states and offer events for switches."""
|
||||||
component = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES)
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES)
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
|
|
||||||
|
@ -132,6 +132,16 @@ async def async_setup(hass, config):
|
||||||
return True
|
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):
|
class SwitchDevice(ToggleEntity):
|
||||||
"""Representation of a switch."""
|
"""Representation of a switch."""
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN,
|
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||||
ATTR_HOME_ID)
|
HMIPC_HAPID)
|
||||||
|
|
||||||
DEPENDENCIES = ['homematicip_cloud']
|
DEPENDENCIES = ['homematicip_cloud']
|
||||||
|
|
||||||
|
@ -24,13 +24,16 @@ ATTR_PROFILE_MODE = 'profile_mode'
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the HomematicIP switch devices."""
|
"""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 (
|
from homematicip.device import (
|
||||||
PlugableSwitch, PlugableSwitchMeasuring,
|
PlugableSwitch, PlugableSwitchMeasuring,
|
||||||
BrandSwitchMeasuring)
|
BrandSwitchMeasuring)
|
||||||
|
|
||||||
if discovery_info is None:
|
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||||
return
|
|
||||||
home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]]
|
|
||||||
devices = []
|
devices = []
|
||||||
for device in home.devices:
|
for device in home.devices:
|
||||||
if isinstance(device, BrandSwitchMeasuring):
|
if isinstance(device, BrandSwitchMeasuring):
|
||||||
|
|
|
@ -127,6 +127,7 @@ HANDLERS = Registry()
|
||||||
FLOWS = [
|
FLOWS = [
|
||||||
'cast',
|
'cast',
|
||||||
'deconz',
|
'deconz',
|
||||||
|
'homematicip_cloud',
|
||||||
'hue',
|
'hue',
|
||||||
'nest',
|
'nest',
|
||||||
'sonos',
|
'sonos',
|
||||||
|
|
|
@ -421,7 +421,7 @@ home-assistant-frontend==20180704.0
|
||||||
# homekit==0.6
|
# homekit==0.6
|
||||||
|
|
||||||
# homeassistant.components.homematicip_cloud
|
# homeassistant.components.homematicip_cloud
|
||||||
homematicip==0.9.4
|
homematicip==0.9.6
|
||||||
|
|
||||||
# homeassistant.components.remember_the_milk
|
# homeassistant.components.remember_the_milk
|
||||||
httplib2==0.10.3
|
httplib2==0.10.3
|
||||||
|
|
|
@ -83,6 +83,9 @@ holidays==0.9.5
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20180704.0
|
home-assistant-frontend==20180704.0
|
||||||
|
|
||||||
|
# homeassistant.components.homematicip_cloud
|
||||||
|
homematicip==0.9.6
|
||||||
|
|
||||||
# homeassistant.components.influxdb
|
# homeassistant.components.influxdb
|
||||||
# homeassistant.components.sensor.influxdb
|
# homeassistant.components.sensor.influxdb
|
||||||
influxdb==5.0.0
|
influxdb==5.0.0
|
||||||
|
|
|
@ -56,6 +56,7 @@ TEST_REQUIREMENTS = (
|
||||||
'hbmqtt',
|
'hbmqtt',
|
||||||
'holidays',
|
'holidays',
|
||||||
'home-assistant-frontend',
|
'home-assistant-frontend',
|
||||||
|
'homematicip',
|
||||||
'influxdb',
|
'influxdb',
|
||||||
'libpurecoollink',
|
'libpurecoollink',
|
||||||
'libsoundtouch',
|
'libsoundtouch',
|
||||||
|
|
1
tests/components/homematicip_cloud/__init__.py
Normal file
1
tests/components/homematicip_cloud/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the HomematicIP Cloud component."""
|
150
tests/components/homematicip_cloud/test_config_flow.py
Normal file
150
tests/components/homematicip_cloud/test_config_flow.py
Normal file
|
@ -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'
|
113
tests/components/homematicip_cloud/test_hap.py
Normal file
113
tests/components/homematicip_cloud/test_hap.py
Normal file
|
@ -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
|
103
tests/components/homematicip_cloud/test_init.py
Normal file
103
tests/components/homematicip_cloud/test_init.py
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue