Config flow for hunterdouglas_powerview (#34795)
Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
6ae7f31947
commit
6c18a2cae2
17 changed files with 1140 additions and 60 deletions
|
@ -311,7 +311,10 @@ omit =
|
|||
homeassistant/components/huawei_lte/*
|
||||
homeassistant/components/huawei_router/device_tracker.py
|
||||
homeassistant/components/hue/light.py
|
||||
homeassistant/components/hunterdouglas_powerview/__init__.py
|
||||
homeassistant/components/hunterdouglas_powerview/scene.py
|
||||
homeassistant/components/hunterdouglas_powerview/cover.py
|
||||
homeassistant/components/hunterdouglas_powerview/entity.py
|
||||
homeassistant/components/hydrawise/*
|
||||
homeassistant/components/hyperion/light.py
|
||||
homeassistant/components/ialarm/alarm_control_panel.py
|
||||
|
|
|
@ -174,6 +174,7 @@ homeassistant/components/http/* @home-assistant/core
|
|||
homeassistant/components/huawei_lte/* @scop
|
||||
homeassistant/components/huawei_router/* @abmantis
|
||||
homeassistant/components/hue/* @balloob
|
||||
homeassistant/components/hunterdouglas_powerview/* @bdraco
|
||||
homeassistant/components/iammeter/* @lewei50
|
||||
homeassistant/components/iaqualink/* @flz
|
||||
homeassistant/components/icloud/* @Quentame
|
||||
|
|
|
@ -1 +1,194 @@
|
|||
"""The hunterdouglas_powerview component."""
|
||||
"""The Hunter Douglas PowerView integration."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiopvapi.helpers.aiorequest import AioRequest
|
||||
from aiopvapi.helpers.constants import ATTR_ID
|
||||
from aiopvapi.helpers.tools import base64_to_unicode
|
||||
from aiopvapi.rooms import Rooms
|
||||
from aiopvapi.scenes import Scenes
|
||||
from aiopvapi.shades import Shades
|
||||
from aiopvapi.userdata import UserData
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
COORDINATOR,
|
||||
DEVICE_FIRMWARE,
|
||||
DEVICE_INFO,
|
||||
DEVICE_MAC_ADDRESS,
|
||||
DEVICE_MODEL,
|
||||
DEVICE_NAME,
|
||||
DEVICE_REVISION,
|
||||
DEVICE_SERIAL_NUMBER,
|
||||
DOMAIN,
|
||||
FIRMWARE_IN_USERDATA,
|
||||
HUB_EXCEPTIONS,
|
||||
HUB_NAME,
|
||||
MAC_ADDRESS_IN_USERDATA,
|
||||
MAINPROCESSOR_IN_USERDATA_FIRMWARE,
|
||||
MODEL_IN_MAINPROCESSOR,
|
||||
PV_API,
|
||||
PV_ROOM_DATA,
|
||||
PV_SCENE_DATA,
|
||||
PV_SHADE_DATA,
|
||||
PV_SHADES,
|
||||
REVISION_IN_MAINPROCESSOR,
|
||||
ROOM_DATA,
|
||||
SCENE_DATA,
|
||||
SERIAL_NUMBER_IN_USERDATA,
|
||||
SHADE_DATA,
|
||||
USER_DATA,
|
||||
)
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def _has_all_unique_hosts(value):
|
||||
"""Validate that each hub configured has a unique host."""
|
||||
hosts = [device[CONF_HOST] for device in value]
|
||||
schema = vol.Schema(vol.Unique())
|
||||
schema(hosts)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_hosts)},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
PLATFORMS = ["cover", "scene"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: dict):
|
||||
"""Set up the Hunter Douglas PowerView component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
if DOMAIN not in hass_config:
|
||||
return True
|
||||
|
||||
for conf in hass_config[DOMAIN]:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=conf
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Hunter Douglas PowerView from a config entry."""
|
||||
|
||||
config = entry.data
|
||||
|
||||
hub_address = config.get(CONF_HOST)
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
device_info = await async_get_device_info(pv_request)
|
||||
except HUB_EXCEPTIONS:
|
||||
_LOGGER.error("Connection error to PowerView hub: %s", hub_address)
|
||||
raise ConfigEntryNotReady
|
||||
if not device_info:
|
||||
_LOGGER.error("Unable to initialize PowerView hub: %s", hub_address)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
rooms = Rooms(pv_request)
|
||||
room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA])
|
||||
|
||||
scenes = Scenes(pv_request)
|
||||
scene_data = _async_map_data_by_id((await scenes.get_resources())[SCENE_DATA])
|
||||
|
||||
shades = Shades(pv_request)
|
||||
shade_data = _async_map_data_by_id((await shades.get_resources())[SHADE_DATA])
|
||||
|
||||
async def async_update_data():
|
||||
"""Fetch data from shade endpoint."""
|
||||
async with async_timeout.timeout(10):
|
||||
shade_entries = await shades.get_resources()
|
||||
if not shade_entries:
|
||||
raise UpdateFailed(f"Failed to fetch new shade data.")
|
||||
return _async_map_data_by_id(shade_entries[SHADE_DATA])
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="powerview hub",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
PV_API: pv_request,
|
||||
PV_ROOM_DATA: room_data,
|
||||
PV_SCENE_DATA: scene_data,
|
||||
PV_SHADES: shades,
|
||||
PV_SHADE_DATA: shade_data,
|
||||
COORDINATOR: coordinator,
|
||||
DEVICE_INFO: device_info,
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_get_device_info(pv_request):
|
||||
"""Determine device info."""
|
||||
userdata = UserData(pv_request)
|
||||
resources = await userdata.get_resources()
|
||||
userdata_data = resources[USER_DATA]
|
||||
|
||||
main_processor_info = userdata_data[FIRMWARE_IN_USERDATA][
|
||||
MAINPROCESSOR_IN_USERDATA_FIRMWARE
|
||||
]
|
||||
return {
|
||||
DEVICE_NAME: base64_to_unicode(userdata_data[HUB_NAME]),
|
||||
DEVICE_MAC_ADDRESS: userdata_data[MAC_ADDRESS_IN_USERDATA],
|
||||
DEVICE_SERIAL_NUMBER: userdata_data[SERIAL_NUMBER_IN_USERDATA],
|
||||
DEVICE_REVISION: main_processor_info[REVISION_IN_MAINPROCESSOR],
|
||||
DEVICE_FIRMWARE: main_processor_info,
|
||||
DEVICE_MODEL: main_processor_info[MODEL_IN_MAINPROCESSOR],
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
def _async_map_data_by_id(data):
|
||||
"""Return a dict with the key being the id for a list of entries."""
|
||||
return {entry[ATTR_ID]: entry for entry in data}
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
|
135
homeassistant/components/hunterdouglas_powerview/config_flow.py
Normal file
135
homeassistant/components/hunterdouglas_powerview/config_flow.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
"""Config flow for Hunter Douglas PowerView integration."""
|
||||
import logging
|
||||
|
||||
from aiopvapi.helpers.aiorequest import AioRequest
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from . import async_get_device_info
|
||||
from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, HUB_EXCEPTIONS
|
||||
from .const import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
||||
HAP_SUFFIX = "._hap._tcp.local."
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
|
||||
hub_address = data[CONF_HOST]
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession)
|
||||
|
||||
try:
|
||||
async with async_timeout.timeout(10):
|
||||
device_info = await async_get_device_info(pv_request)
|
||||
except HUB_EXCEPTIONS:
|
||||
raise CannotConnect
|
||||
if not device_info:
|
||||
raise CannotConnect
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
return {
|
||||
"title": device_info[DEVICE_NAME],
|
||||
"unique_id": device_info[DEVICE_SERIAL_NUMBER],
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Hunter Douglas PowerView."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the powerview config flow."""
|
||||
self.powerview_config = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
if self._host_already_configured(user_input[CONF_HOST]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
await self.async_set_unique_id(info["unique_id"])
|
||||
return self.async_create_entry(
|
||||
title=info["title"], data={CONF_HOST: user_input[CONF_HOST]}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_homekit(self, homekit_info):
|
||||
"""Handle HomeKit discovery."""
|
||||
|
||||
# If we already have the host configured do
|
||||
# not open connections to it if we can avoid it.
|
||||
if self._host_already_configured(homekit_info[CONF_HOST]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, homekit_info)
|
||||
except CannotConnect:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
await self.async_set_unique_id(info["unique_id"], raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: homekit_info["host"]})
|
||||
|
||||
name = homekit_info["name"]
|
||||
if name.endswith(HAP_SUFFIX):
|
||||
name = name[: -len(HAP_SUFFIX)]
|
||||
|
||||
self.powerview_config = {
|
||||
CONF_HOST: homekit_info["host"],
|
||||
CONF_NAME: name,
|
||||
}
|
||||
return await self.async_step_link()
|
||||
|
||||
async def async_step_link(self, user_input=None):
|
||||
"""Attempt to link with Powerview."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.powerview_config[CONF_NAME],
|
||||
data={CONF_HOST: self.powerview_config[CONF_HOST]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="link", description_placeholders=self.powerview_config
|
||||
)
|
||||
|
||||
def _host_already_configured(self, host):
|
||||
"""See if we already have a hub with the host address configured."""
|
||||
existing_hosts = {
|
||||
entry.data[CONF_HOST] for entry in self._async_current_entries()
|
||||
}
|
||||
return host in existing_hosts
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
65
homeassistant/components/hunterdouglas_powerview/const.py
Normal file
65
homeassistant/components/hunterdouglas_powerview/const.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
"""Support for Powerview scenes from a Powerview hub."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiopvapi.helpers.aiorequest import PvApiConnectionError
|
||||
|
||||
DOMAIN = "hunterdouglas_powerview"
|
||||
|
||||
|
||||
MANUFACTURER = "Hunter Douglas"
|
||||
|
||||
HUB_ADDRESS = "address"
|
||||
|
||||
SCENE_DATA = "sceneData"
|
||||
SHADE_DATA = "shadeData"
|
||||
ROOM_DATA = "roomData"
|
||||
USER_DATA = "userData"
|
||||
|
||||
MAC_ADDRESS_IN_USERDATA = "macAddress"
|
||||
SERIAL_NUMBER_IN_USERDATA = "serialNumber"
|
||||
FIRMWARE_IN_USERDATA = "firmware"
|
||||
MAINPROCESSOR_IN_USERDATA_FIRMWARE = "mainProcessor"
|
||||
REVISION_IN_MAINPROCESSOR = "revision"
|
||||
MODEL_IN_MAINPROCESSOR = "name"
|
||||
HUB_NAME = "hubName"
|
||||
|
||||
FIRMWARE_IN_SHADE = "firmware"
|
||||
|
||||
FIRMWARE_REVISION = "revision"
|
||||
FIRMWARE_SUB_REVISION = "subRevision"
|
||||
FIRMWARE_BUILD = "build"
|
||||
|
||||
DEVICE_NAME = "device_name"
|
||||
DEVICE_MAC_ADDRESS = "device_mac_address"
|
||||
DEVICE_SERIAL_NUMBER = "device_serial_number"
|
||||
DEVICE_REVISION = "device_revision"
|
||||
DEVICE_INFO = "device_info"
|
||||
DEVICE_MODEL = "device_model"
|
||||
DEVICE_FIRMWARE = "device_firmware"
|
||||
|
||||
SCENE_NAME = "name"
|
||||
SCENE_ID = "id"
|
||||
ROOM_ID_IN_SCENE = "roomId"
|
||||
|
||||
SHADE_NAME = "name"
|
||||
SHADE_ID = "id"
|
||||
ROOM_ID_IN_SHADE = "roomId"
|
||||
|
||||
ROOM_NAME = "name"
|
||||
ROOM_NAME_UNICODE = "name_unicode"
|
||||
ROOM_ID = "id"
|
||||
|
||||
SHADE_RESPONSE = "shade"
|
||||
|
||||
STATE_ATTRIBUTE_ROOM_NAME = "roomName"
|
||||
|
||||
PV_API = "pv_api"
|
||||
PV_HUB = "pv_hub"
|
||||
PV_SHADES = "pv_shades"
|
||||
PV_SCENE_DATA = "pv_scene_data"
|
||||
PV_SHADE_DATA = "pv_shade_data"
|
||||
PV_ROOM_DATA = "pv_room_data"
|
||||
COORDINATOR = "coordinator"
|
||||
|
||||
HUB_EXCEPTIONS = (asyncio.TimeoutError, PvApiConnectionError)
|
306
homeassistant/components/hunterdouglas_powerview/cover.py
Normal file
306
homeassistant/components/hunterdouglas_powerview/cover.py
Normal file
|
@ -0,0 +1,306 @@
|
|||
"""Support for hunter douglas shades."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA
|
||||
from aiopvapi.resources.shade import (
|
||||
ATTR_POSKIND1,
|
||||
ATTR_TYPE,
|
||||
MAX_POSITION,
|
||||
MIN_POSITION,
|
||||
factory as PvShade,
|
||||
)
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DEVICE_CLASS_SHADE,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_SET_POSITION,
|
||||
SUPPORT_STOP,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import (
|
||||
COORDINATOR,
|
||||
DEVICE_INFO,
|
||||
DEVICE_MODEL,
|
||||
DEVICE_SERIAL_NUMBER,
|
||||
DOMAIN,
|
||||
FIRMWARE_BUILD,
|
||||
FIRMWARE_IN_SHADE,
|
||||
FIRMWARE_REVISION,
|
||||
FIRMWARE_SUB_REVISION,
|
||||
MANUFACTURER,
|
||||
PV_API,
|
||||
PV_ROOM_DATA,
|
||||
PV_SHADE_DATA,
|
||||
ROOM_ID_IN_SHADE,
|
||||
ROOM_NAME_UNICODE,
|
||||
SHADE_RESPONSE,
|
||||
STATE_ATTRIBUTE_ROOM_NAME,
|
||||
)
|
||||
from .entity import HDEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Estimated time it takes to complete a transition
|
||||
# from one state to another
|
||||
TRANSITION_COMPLETE_DURATION = 30
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up the hunter douglas shades."""
|
||||
|
||||
pv_data = hass.data[DOMAIN][entry.entry_id]
|
||||
room_data = pv_data[PV_ROOM_DATA]
|
||||
shade_data = pv_data[PV_SHADE_DATA]
|
||||
pv_request = pv_data[PV_API]
|
||||
coordinator = pv_data[COORDINATOR]
|
||||
device_info = pv_data[DEVICE_INFO]
|
||||
|
||||
entities = []
|
||||
for raw_shade in shade_data.values():
|
||||
# The shade may be out of sync with the hub
|
||||
# so we force a refresh when we add it if
|
||||
# possible
|
||||
shade = PvShade(raw_shade, pv_request)
|
||||
name_before_refresh = shade.name
|
||||
try:
|
||||
async with async_timeout.timeout(1):
|
||||
await shade.refresh()
|
||||
except asyncio.TimeoutError:
|
||||
# Forced refresh is not required for setup
|
||||
pass
|
||||
entities.append(
|
||||
PowerViewShade(
|
||||
shade, name_before_refresh, room_data, coordinator, device_info
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
def hd_position_to_hass(hd_position):
|
||||
"""Convert hunter douglas position to hass position."""
|
||||
return round((hd_position / MAX_POSITION) * 100)
|
||||
|
||||
|
||||
def hass_position_to_hd(hass_positon):
|
||||
"""Convert hass position to hunter douglas position."""
|
||||
return int(hass_positon / 100 * MAX_POSITION)
|
||||
|
||||
|
||||
class PowerViewShade(HDEntity, CoverEntity):
|
||||
"""Representation of a powerview shade."""
|
||||
|
||||
def __init__(self, shade, name, room_data, coordinator, device_info):
|
||||
"""Initialize the shade."""
|
||||
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
|
||||
super().__init__(coordinator, device_info, shade.id)
|
||||
self._shade = shade
|
||||
self._device_info = device_info
|
||||
self._is_opening = False
|
||||
self._is_closing = False
|
||||
self._room_name = None
|
||||
self._last_action_timestamp = 0
|
||||
self._scheduled_transition_update = None
|
||||
self._name = name
|
||||
self._room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
|
||||
self._current_cover_position = MIN_POSITION
|
||||
self._coordinator = coordinator
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
if self._device_info[DEVICE_MODEL] != "1":
|
||||
supported_features |= SUPPORT_STOP
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._current_cover_position == MIN_POSITION
|
||||
|
||||
@property
|
||||
def is_opening(self):
|
||||
"""Return if the cover is opening."""
|
||||
return self._is_opening
|
||||
|
||||
@property
|
||||
def is_closing(self):
|
||||
"""Return if the cover is closing."""
|
||||
return self._is_closing
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of cover."""
|
||||
return hd_position_to_hass(self._current_cover_position)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return device class."""
|
||||
return DEVICE_CLASS_SHADE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the shade."""
|
||||
return self._name
|
||||
|
||||
async def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
await self._async_move(0)
|
||||
|
||||
async def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
await self._async_move(100)
|
||||
|
||||
async def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
# Cancel any previous updates
|
||||
self._async_cancel_scheduled_transition_update()
|
||||
self._async_update_from_command(await self._shade.stop())
|
||||
await self._async_force_refresh_state()
|
||||
|
||||
async def set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION not in kwargs:
|
||||
return
|
||||
await self._async_move(kwargs[ATTR_POSITION])
|
||||
|
||||
async def _async_move(self, target_hass_position):
|
||||
"""Move the shade to a position."""
|
||||
current_hass_position = hd_position_to_hass(self._current_cover_position)
|
||||
steps_to_move = abs(current_hass_position - target_hass_position)
|
||||
if not steps_to_move:
|
||||
return
|
||||
self._async_schedule_update_for_transition(steps_to_move)
|
||||
self._async_update_from_command(
|
||||
await self._shade.move(
|
||||
{
|
||||
ATTR_POSITION1: hass_position_to_hd(target_hass_position),
|
||||
ATTR_POSKIND1: 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
self._is_opening = False
|
||||
self._is_closing = False
|
||||
if target_hass_position > current_hass_position:
|
||||
self._is_opening = True
|
||||
elif target_hass_position < current_hass_position:
|
||||
self._is_closing = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_command(self, raw_data):
|
||||
"""Update the shade state after a command."""
|
||||
if not raw_data or SHADE_RESPONSE not in raw_data:
|
||||
return
|
||||
self._async_process_new_shade_data(raw_data[SHADE_RESPONSE])
|
||||
|
||||
@callback
|
||||
def _async_process_new_shade_data(self, data):
|
||||
"""Process new data from an update."""
|
||||
self._shade.raw_data = data
|
||||
self._async_update_current_cover_position()
|
||||
|
||||
@callback
|
||||
def _async_update_current_cover_position(self):
|
||||
"""Update the current cover position from the data."""
|
||||
_LOGGER.debug("Raw data update: %s", self._shade.raw_data)
|
||||
position_data = self._shade.raw_data[ATTR_POSITION_DATA]
|
||||
if ATTR_POSITION1 in position_data:
|
||||
self._current_cover_position = position_data[ATTR_POSITION1]
|
||||
self._is_opening = False
|
||||
self._is_closing = False
|
||||
|
||||
@callback
|
||||
def _async_cancel_scheduled_transition_update(self):
|
||||
"""Cancel any previous updates."""
|
||||
if not self._scheduled_transition_update:
|
||||
return
|
||||
self._scheduled_transition_update()
|
||||
self._scheduled_transition_update = None
|
||||
|
||||
@callback
|
||||
def _async_schedule_update_for_transition(self, steps):
|
||||
self.async_write_ha_state()
|
||||
|
||||
# Cancel any previous updates
|
||||
self._async_cancel_scheduled_transition_update()
|
||||
|
||||
est_time_to_complete_transition = 1 + int(
|
||||
TRANSITION_COMPLETE_DURATION * (steps / 100)
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Estimated time to complete transition of %s steps for %s: %s",
|
||||
steps,
|
||||
self.name,
|
||||
est_time_to_complete_transition,
|
||||
)
|
||||
|
||||
# Schedule an update for when we expect the transition
|
||||
# to be completed.
|
||||
self._scheduled_transition_update = async_call_later(
|
||||
self.hass,
|
||||
est_time_to_complete_transition,
|
||||
self._async_complete_schedule_update,
|
||||
)
|
||||
|
||||
async def _async_complete_schedule_update(self, _):
|
||||
"""Update status of the cover."""
|
||||
_LOGGER.debug("Processing scheduled update for %s", self.name)
|
||||
self._scheduled_transition_update = None
|
||||
await self._async_force_refresh_state()
|
||||
|
||||
async def _async_force_refresh_state(self):
|
||||
"""Refresh the cover state and force the device cache to be bypassed."""
|
||||
await self._shade.refresh()
|
||||
self._async_update_current_cover_position()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
firmware = self._shade.raw_data[FIRMWARE_IN_SHADE]
|
||||
sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}"
|
||||
model = self._shade.raw_data[ATTR_TYPE]
|
||||
for shade in self._shade.shade_types:
|
||||
if shade.shade_type == model:
|
||||
model = shade.description
|
||||
break
|
||||
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"name": self.name,
|
||||
"model": str(model),
|
||||
"sw_version": sw_version,
|
||||
"manufacturer": MANUFACTURER,
|
||||
"via_device": (DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]),
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""When entity is added to hass."""
|
||||
self._async_update_current_cover_position()
|
||||
self.async_on_remove(
|
||||
self._coordinator.async_add_listener(self._async_update_shade_from_group)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_update_shade_from_group(self):
|
||||
"""Update with new data from the coordinator."""
|
||||
if self._scheduled_transition_update:
|
||||
# If a transition in in progress
|
||||
# the data will be wrong
|
||||
return
|
||||
self._async_process_new_shade_data(self._coordinator.data[self._shade.id])
|
||||
self.async_write_ha_state()
|
59
homeassistant/components/hunterdouglas_powerview/entity.py
Normal file
59
homeassistant/components/hunterdouglas_powerview/entity.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
"""The nexia integration base entity."""
|
||||
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import (
|
||||
DEVICE_FIRMWARE,
|
||||
DEVICE_MAC_ADDRESS,
|
||||
DEVICE_MODEL,
|
||||
DEVICE_NAME,
|
||||
DEVICE_SERIAL_NUMBER,
|
||||
DOMAIN,
|
||||
FIRMWARE_BUILD,
|
||||
FIRMWARE_REVISION,
|
||||
FIRMWARE_SUB_REVISION,
|
||||
MANUFACTURER,
|
||||
)
|
||||
|
||||
|
||||
class HDEntity(Entity):
|
||||
"""Base class for hunter douglas entities."""
|
||||
|
||||
def __init__(self, coordinator, device_info, unique_id):
|
||||
"""Initialize the entity."""
|
||||
super().__init__()
|
||||
self._coordinator = coordinator
|
||||
self._unique_id = unique_id
|
||||
self._device_info = device_info
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return False, updates are controlled via coordinator."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return the device_info of the device."""
|
||||
firmware = self._device_info[DEVICE_FIRMWARE]
|
||||
sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}"
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])},
|
||||
"connections": {
|
||||
(dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS])
|
||||
},
|
||||
"name": self._device_info[DEVICE_NAME],
|
||||
"model": self._device_info[DEVICE_MODEL],
|
||||
"sw_version": sw_version,
|
||||
"manufacturer": MANUFACTURER,
|
||||
}
|
|
@ -2,6 +2,12 @@
|
|||
"domain": "hunterdouglas_powerview",
|
||||
"name": "Hunter Douglas PowerView",
|
||||
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
|
||||
"requirements": ["aiopvapi==1.6.14"],
|
||||
"codeowners": []
|
||||
"requirements": [
|
||||
"aiopvapi==1.6.14"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"homekit": {
|
||||
"models": ["PowerView"]
|
||||
}
|
||||
}
|
|
@ -2,86 +2,73 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiopvapi.helpers.aiorequest import AioRequest
|
||||
from aiopvapi.resources.scene import Scene as PvScene
|
||||
from aiopvapi.rooms import Rooms
|
||||
from aiopvapi.scenes import Scenes
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.scene import DOMAIN, Scene
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.components.scene import Scene
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import CONF_HOST, CONF_PLATFORM
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
|
||||
from .const import (
|
||||
COORDINATOR,
|
||||
DEVICE_INFO,
|
||||
DOMAIN,
|
||||
HUB_ADDRESS,
|
||||
PV_API,
|
||||
PV_ROOM_DATA,
|
||||
PV_SCENE_DATA,
|
||||
ROOM_NAME_UNICODE,
|
||||
STATE_ATTRIBUTE_ROOM_NAME,
|
||||
)
|
||||
from .entity import HDEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
HUB_ADDRESS = "address"
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "hunterdouglas_powerview",
|
||||
vol.Required(HUB_ADDRESS): cv.string,
|
||||
}
|
||||
{vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(HUB_ADDRESS): cv.string}
|
||||
)
|
||||
|
||||
|
||||
SCENE_DATA = "sceneData"
|
||||
ROOM_DATA = "roomData"
|
||||
SCENE_NAME = "name"
|
||||
ROOM_NAME = "name"
|
||||
SCENE_ID = "id"
|
||||
ROOM_ID = "id"
|
||||
ROOM_ID_IN_SCENE = "roomId"
|
||||
STATE_ATTRIBUTE_ROOM_NAME = "roomName"
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Import platform from yaml."""
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_HOST: config[HUB_ADDRESS]},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up Home Assistant scene entries."""
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up powerview scene entries."""
|
||||
|
||||
hub_address = config.get(HUB_ADDRESS)
|
||||
websession = async_get_clientsession(hass)
|
||||
pv_data = hass.data[DOMAIN][entry.entry_id]
|
||||
room_data = pv_data[PV_ROOM_DATA]
|
||||
scene_data = pv_data[PV_SCENE_DATA]
|
||||
pv_request = pv_data[PV_API]
|
||||
coordinator = pv_data[COORDINATOR]
|
||||
device_info = pv_data[DEVICE_INFO]
|
||||
|
||||
pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession)
|
||||
|
||||
_scenes = await Scenes(pv_request).get_resources()
|
||||
_rooms = await Rooms(pv_request).get_resources()
|
||||
|
||||
if not _scenes or not _rooms:
|
||||
_LOGGER.error("Unable to initialize PowerView hub: %s", hub_address)
|
||||
return
|
||||
pvscenes = (
|
||||
PowerViewScene(hass, PvScene(_raw_scene, pv_request), _rooms)
|
||||
for _raw_scene in _scenes[SCENE_DATA]
|
||||
PowerViewScene(
|
||||
PvScene(raw_scene, pv_request), room_data, coordinator, device_info
|
||||
)
|
||||
for scene_id, raw_scene in scene_data.items()
|
||||
)
|
||||
async_add_entities(pvscenes)
|
||||
|
||||
|
||||
class PowerViewScene(Scene):
|
||||
class PowerViewScene(HDEntity, Scene):
|
||||
"""Representation of a Powerview scene."""
|
||||
|
||||
def __init__(self, hass, scene, room_data):
|
||||
def __init__(self, scene, room_data, coordinator, device_info):
|
||||
"""Initialize the scene."""
|
||||
super().__init__(coordinator, device_info, scene.id)
|
||||
self._scene = scene
|
||||
self.hass = hass
|
||||
self._room_name = None
|
||||
self._sync_room_data(room_data)
|
||||
self.entity_id = async_generate_entity_id(
|
||||
ENTITY_ID_FORMAT, str(self._scene.id), hass=hass
|
||||
)
|
||||
|
||||
def _sync_room_data(self, room_data):
|
||||
"""Sync room data."""
|
||||
room = next(
|
||||
(
|
||||
room
|
||||
for room in room_data[ROOM_DATA]
|
||||
if room[ROOM_ID] == self._scene.room_id
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
||||
self._room_name = room.get(ROOM_NAME, "")
|
||||
self._room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "")
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"title": "Hunter Douglas PowerView",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the PowerView Hub",
|
||||
"data": {
|
||||
"host": "IP Address"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Connect to the PowerView Hub",
|
||||
"description": "Do you want to setup {name} ({host})?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the PowerView Hub",
|
||||
"data": {
|
||||
"host": "IP Address"
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Connect to the PowerView Hub",
|
||||
"description": "Do you want to setup {name} ({host})?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"unknown": "Unexpected error"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,6 +54,7 @@ FLOWS = [
|
|||
"homematicip_cloud",
|
||||
"huawei_lte",
|
||||
"hue",
|
||||
"hunterdouglas_powerview",
|
||||
"iaqualink",
|
||||
"icloud",
|
||||
"ifttt",
|
||||
|
|
|
@ -49,6 +49,7 @@ HOMEKIT = {
|
|||
"Healty Home Coach": "netatmo",
|
||||
"LIFX": "lifx",
|
||||
"Netatmo Relay": "netatmo",
|
||||
"PowerView": "hunterdouglas_powerview",
|
||||
"Presence": "netatmo",
|
||||
"Rachio": "rachio",
|
||||
"TRADFRI": "tradfri",
|
||||
|
|
|
@ -85,6 +85,9 @@ aiohue==2.1.0
|
|||
# homeassistant.components.notion
|
||||
aionotion==1.1.0
|
||||
|
||||
# homeassistant.components.hunterdouglas_powerview
|
||||
aiopvapi==1.6.14
|
||||
|
||||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==1.0.2
|
||||
|
||||
|
|
1
tests/components/hunterdouglas_powerview/__init__.py
Normal file
1
tests/components/hunterdouglas_powerview/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Hunter Douglas PowerView integration."""
|
220
tests/components/hunterdouglas_powerview/test_config_flow.py
Normal file
220
tests/components/hunterdouglas_powerview/test_config_flow.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
"""Test the Logitech Harmony Hub config flow."""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from asynctest import CoroutineMock, MagicMock, patch
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
def _get_mock_powerview_userdata(userdata=None, get_resources=None):
|
||||
mock_powerview_userdata = MagicMock()
|
||||
if not userdata:
|
||||
userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json"))
|
||||
if get_resources:
|
||||
type(mock_powerview_userdata).get_resources = CoroutineMock(
|
||||
side_effect=get_resources
|
||||
)
|
||||
else:
|
||||
type(mock_powerview_userdata).get_resources = CoroutineMock(
|
||||
return_value=userdata
|
||||
)
|
||||
return mock_powerview_userdata
|
||||
|
||||
|
||||
async def test_user_form(hass):
|
||||
"""Test we get the user form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
), patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"host": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "AlexanderHD"
|
||||
assert result2["data"] == {
|
||||
"host": "1.2.3.4",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
result3 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result3["type"] == "form"
|
||||
assert result3["errors"] == {}
|
||||
|
||||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result3["flow_id"], {"host": "1.2.3.4"},
|
||||
)
|
||||
assert result4["type"] == "abort"
|
||||
|
||||
|
||||
async def test_form_import(hass):
|
||||
"""Test we get the form with import source."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
), patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={"host": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "AlexanderHD"
|
||||
assert result["data"] == {
|
||||
"host": "1.2.3.4",
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_homekit(hass):
|
||||
"""Test we get the form with homekit source."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "homekit"},
|
||||
data={
|
||||
"host": "1.2.3.4",
|
||||
"properties": {"id": "AA::BB::CC::DD::EE::FF"},
|
||||
"name": "PowerViewHub._hap._tcp.local.",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "link"
|
||||
assert result["errors"] is None
|
||||
assert result["description_placeholders"] == {
|
||||
"host": "1.2.3.4",
|
||||
"name": "PowerViewHub",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
), patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup",
|
||||
return_value=True,
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "PowerViewHub"
|
||||
assert result2["data"] == {"host": "1.2.3.4"}
|
||||
assert result2["result"].unique_id == "ABC123"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
result3 = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": "homekit"},
|
||||
data={
|
||||
"host": "1.2.3.4",
|
||||
"properties": {"id": "AA::BB::CC::DD::EE::FF"},
|
||||
"name": "PowerViewHub._hap._tcp.local.",
|
||||
},
|
||||
)
|
||||
assert result3["type"] == "abort"
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata(
|
||||
get_resources=asyncio.TimeoutError
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"host": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_no_data(hass):
|
||||
"""Test we handle no data being returned from the hub."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}})
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"host": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_unknown_exception(hass):
|
||||
"""Test we handle unknown exception."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}})
|
||||
with patch(
|
||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
||||
return_value=mock_powerview_userdata,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"host": "1.2.3.4"},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
50
tests/fixtures/hunterdouglas_powerview/userdata.json
vendored
Normal file
50
tests/fixtures/hunterdouglas_powerview/userdata.json
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"userData": {
|
||||
"_id": "abc",
|
||||
"color": {
|
||||
"green": 0,
|
||||
"blue": 255,
|
||||
"brightness": 5,
|
||||
"red": 0
|
||||
},
|
||||
"autoBackup": false,
|
||||
"ip": "192.168.1.72",
|
||||
"macAddress": "aa:bb:cc:dd:ee:ff",
|
||||
"mask": "255.255.255.0",
|
||||
"gateway": "192.168.1.1",
|
||||
"dns": "192.168.1.3",
|
||||
"firmware": {
|
||||
"mainProcessor": {
|
||||
"name": "PV Hub2.0",
|
||||
"revision": 2,
|
||||
"subRevision": 0,
|
||||
"build": 1024
|
||||
},
|
||||
"radio": {
|
||||
"revision": 2,
|
||||
"subRevision": 0,
|
||||
"build": 2610
|
||||
}
|
||||
},
|
||||
"serialNumber": "ABC123",
|
||||
"rfIDInt": 64789,
|
||||
"rfID": "0xFD15",
|
||||
"rfStatus": 0,
|
||||
"brand": "HD",
|
||||
"wireless": false,
|
||||
"hubName": "QWxleGFuZGVySEQ=",
|
||||
"localTimeDataSet": true,
|
||||
"enableScheduledEvents": true,
|
||||
"editingEnabled": true,
|
||||
"setupCompleted": false,
|
||||
"staticIp": false,
|
||||
"times": {
|
||||
"timezone": "America/Chicago",
|
||||
"localSunriseTimeInMinutes": 0,
|
||||
"localSunsetTimeInMinutes": 0,
|
||||
"currentOffset": -18000
|
||||
},
|
||||
"rcUp": true,
|
||||
"remoteConnectEnabled": true
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue