mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Config flow for hunterdouglas_powerview (#34795)
Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
6ae7f31947
commit
6c18a2cae2
@ -311,7 +311,10 @@ omit =
|
|||||||
homeassistant/components/huawei_lte/*
|
homeassistant/components/huawei_lte/*
|
||||||
homeassistant/components/huawei_router/device_tracker.py
|
homeassistant/components/huawei_router/device_tracker.py
|
||||||
homeassistant/components/hue/light.py
|
homeassistant/components/hue/light.py
|
||||||
|
homeassistant/components/hunterdouglas_powerview/__init__.py
|
||||||
homeassistant/components/hunterdouglas_powerview/scene.py
|
homeassistant/components/hunterdouglas_powerview/scene.py
|
||||||
|
homeassistant/components/hunterdouglas_powerview/cover.py
|
||||||
|
homeassistant/components/hunterdouglas_powerview/entity.py
|
||||||
homeassistant/components/hydrawise/*
|
homeassistant/components/hydrawise/*
|
||||||
homeassistant/components/hyperion/light.py
|
homeassistant/components/hyperion/light.py
|
||||||
homeassistant/components/ialarm/alarm_control_panel.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_lte/* @scop
|
||||||
homeassistant/components/huawei_router/* @abmantis
|
homeassistant/components/huawei_router/* @abmantis
|
||||||
homeassistant/components/hue/* @balloob
|
homeassistant/components/hue/* @balloob
|
||||||
|
homeassistant/components/hunterdouglas_powerview/* @bdraco
|
||||||
homeassistant/components/iammeter/* @lewei50
|
homeassistant/components/iammeter/* @lewei50
|
||||||
homeassistant/components/iaqualink/* @flz
|
homeassistant/components/iaqualink/* @flz
|
||||||
homeassistant/components/icloud/* @Quentame
|
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",
|
"domain": "hunterdouglas_powerview",
|
||||||
"name": "Hunter Douglas PowerView",
|
"name": "Hunter Douglas PowerView",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
|
"documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview",
|
||||||
"requirements": ["aiopvapi==1.6.14"],
|
"requirements": [
|
||||||
"codeowners": []
|
"aiopvapi==1.6.14"
|
||||||
}
|
],
|
||||||
|
"codeowners": ["@bdraco"],
|
||||||
|
"config_flow": true,
|
||||||
|
"homekit": {
|
||||||
|
"models": ["PowerView"]
|
||||||
|
}
|
||||||
|
}
|
@ -2,86 +2,73 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiopvapi.helpers.aiorequest import AioRequest
|
|
||||||
from aiopvapi.resources.scene import Scene as PvScene
|
from aiopvapi.resources.scene import Scene as PvScene
|
||||||
from aiopvapi.rooms import Rooms
|
|
||||||
from aiopvapi.scenes import Scenes
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.scene import DOMAIN, Scene
|
from homeassistant.components.scene import Scene
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.const import CONF_HOST, CONF_PLATFORM
|
||||||
import homeassistant.helpers.config_validation as cv
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
||||||
HUB_ADDRESS = "address"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema(
|
PLATFORM_SCHEMA = vol.Schema(
|
||||||
{
|
{vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(HUB_ADDRESS): cv.string}
|
||||||
vol.Required(CONF_PLATFORM): "hunterdouglas_powerview",
|
|
||||||
vol.Required(HUB_ADDRESS): cv.string,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
SCENE_DATA = "sceneData"
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
ROOM_DATA = "roomData"
|
"""Import platform from yaml."""
|
||||||
SCENE_NAME = "name"
|
|
||||||
ROOM_NAME = "name"
|
hass.async_create_task(
|
||||||
SCENE_ID = "id"
|
hass.config_entries.flow.async_init(
|
||||||
ROOM_ID = "id"
|
DOMAIN,
|
||||||
ROOM_ID_IN_SCENE = "roomId"
|
context={"source": SOURCE_IMPORT},
|
||||||
STATE_ATTRIBUTE_ROOM_NAME = "roomName"
|
data={CONF_HOST: config[HUB_ADDRESS]},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_entry(hass, entry, async_add_entities):
|
||||||
"""Set up Home Assistant scene entries."""
|
"""Set up powerview scene entries."""
|
||||||
|
|
||||||
hub_address = config.get(HUB_ADDRESS)
|
pv_data = hass.data[DOMAIN][entry.entry_id]
|
||||||
websession = async_get_clientsession(hass)
|
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 = (
|
pvscenes = (
|
||||||
PowerViewScene(hass, PvScene(_raw_scene, pv_request), _rooms)
|
PowerViewScene(
|
||||||
for _raw_scene in _scenes[SCENE_DATA]
|
PvScene(raw_scene, pv_request), room_data, coordinator, device_info
|
||||||
|
)
|
||||||
|
for scene_id, raw_scene in scene_data.items()
|
||||||
)
|
)
|
||||||
async_add_entities(pvscenes)
|
async_add_entities(pvscenes)
|
||||||
|
|
||||||
|
|
||||||
class PowerViewScene(Scene):
|
class PowerViewScene(HDEntity, Scene):
|
||||||
"""Representation of a Powerview 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."""
|
"""Initialize the scene."""
|
||||||
|
super().__init__(coordinator, device_info, scene.id)
|
||||||
self._scene = scene
|
self._scene = scene
|
||||||
self.hass = hass
|
self._room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "")
|
||||||
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, "")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
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",
|
"homematicip_cloud",
|
||||||
"huawei_lte",
|
"huawei_lte",
|
||||||
"hue",
|
"hue",
|
||||||
|
"hunterdouglas_powerview",
|
||||||
"iaqualink",
|
"iaqualink",
|
||||||
"icloud",
|
"icloud",
|
||||||
"ifttt",
|
"ifttt",
|
||||||
|
@ -49,6 +49,7 @@ HOMEKIT = {
|
|||||||
"Healty Home Coach": "netatmo",
|
"Healty Home Coach": "netatmo",
|
||||||
"LIFX": "lifx",
|
"LIFX": "lifx",
|
||||||
"Netatmo Relay": "netatmo",
|
"Netatmo Relay": "netatmo",
|
||||||
|
"PowerView": "hunterdouglas_powerview",
|
||||||
"Presence": "netatmo",
|
"Presence": "netatmo",
|
||||||
"Rachio": "rachio",
|
"Rachio": "rachio",
|
||||||
"TRADFRI": "tradfri",
|
"TRADFRI": "tradfri",
|
||||||
|
@ -85,6 +85,9 @@ aiohue==2.1.0
|
|||||||
# homeassistant.components.notion
|
# homeassistant.components.notion
|
||||||
aionotion==1.1.0
|
aionotion==1.1.0
|
||||||
|
|
||||||
|
# homeassistant.components.hunterdouglas_powerview
|
||||||
|
aiopvapi==1.6.14
|
||||||
|
|
||||||
# homeassistant.components.pvpc_hourly_pricing
|
# homeassistant.components.pvpc_hourly_pricing
|
||||||
aiopvpc==1.0.2
|
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…
x
Reference in New Issue
Block a user