mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Powerview Gen 3 functionality (#110158)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
d6efdc47a5
commit
3529eb6044
@ -3,40 +3,23 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiopvapi.helpers.aiorequest import AioRequest
|
from aiopvapi.helpers.aiorequest import AioRequest
|
||||||
from aiopvapi.helpers.api_base import ApiEntryPoint
|
from aiopvapi.hub import Hub
|
||||||
from aiopvapi.helpers.tools import base64_to_unicode
|
from aiopvapi.resources.model import PowerviewData
|
||||||
from aiopvapi.rooms import Rooms
|
from aiopvapi.rooms import Rooms
|
||||||
from aiopvapi.scenes import Scenes
|
from aiopvapi.scenes import Scenes
|
||||||
from aiopvapi.shades import Shades
|
from aiopvapi.shades import Shades
|
||||||
from aiopvapi.userdata import UserData
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN, HUB_EXCEPTIONS
|
||||||
API_PATH_FWVERSION,
|
|
||||||
DEFAULT_LEGACY_MAINPROCESSOR,
|
|
||||||
DOMAIN,
|
|
||||||
FIRMWARE,
|
|
||||||
FIRMWARE_MAINPROCESSOR,
|
|
||||||
FIRMWARE_NAME,
|
|
||||||
HUB_EXCEPTIONS,
|
|
||||||
HUB_NAME,
|
|
||||||
MAC_ADDRESS_IN_USERDATA,
|
|
||||||
ROOM_DATA,
|
|
||||||
SCENE_DATA,
|
|
||||||
SERIAL_NUMBER_IN_USERDATA,
|
|
||||||
SHADE_DATA,
|
|
||||||
USER_DATA,
|
|
||||||
)
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
from .shade_data import PowerviewShadeData
|
from .shade_data import PowerviewShadeData
|
||||||
from .util import async_map_data_by_id
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@ -58,46 +41,70 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
config = entry.data
|
config = entry.data
|
||||||
|
|
||||||
hub_address = config[CONF_HOST]
|
hub_address = config[CONF_HOST]
|
||||||
|
api_version = config.get(CONF_API_VERSION, None)
|
||||||
|
_LOGGER.debug("Connecting %s at %s with v%s api", DOMAIN, hub_address, api_version)
|
||||||
|
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(hass)
|
||||||
|
|
||||||
pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession)
|
pv_request = AioRequest(
|
||||||
|
hub_address, loop=hass.loop, websession=websession, api_version=api_version
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(10):
|
||||||
device_info = await async_get_device_info(pv_request, hub_address)
|
hub = Hub(pv_request)
|
||||||
|
await hub.query_firmware()
|
||||||
async with asyncio.timeout(10):
|
device_info = await async_get_device_info(hub)
|
||||||
rooms = Rooms(pv_request)
|
|
||||||
room_data = async_map_data_by_id((await rooms.get_resources())[ROOM_DATA])
|
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
|
||||||
scenes = Scenes(pv_request)
|
|
||||||
scene_data = async_map_data_by_id(
|
|
||||||
(await scenes.get_resources())[SCENE_DATA]
|
|
||||||
)
|
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
|
||||||
shades = Shades(pv_request)
|
|
||||||
shade_entries = await shades.get_resources()
|
|
||||||
shade_data = async_map_data_by_id(shade_entries[SHADE_DATA])
|
|
||||||
|
|
||||||
except HUB_EXCEPTIONS as err:
|
except HUB_EXCEPTIONS as err:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
f"Connection error to PowerView hub: {hub_address}: {err}"
|
f"Connection error to PowerView hub {hub_address}: {err}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
if hub.role != "Primary":
|
||||||
|
# this should be caught in config_flow, but account for a hub changing roles
|
||||||
|
# this will only happen manually by a user
|
||||||
|
_LOGGER.error(
|
||||||
|
"%s (%s) is performing role of %s Hub. "
|
||||||
|
"Only the Primary Hub can manage shades",
|
||||||
|
hub.name,
|
||||||
|
hub.hub_address,
|
||||||
|
hub.role,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
rooms = Rooms(pv_request)
|
||||||
|
room_data: PowerviewData = await rooms.get_rooms()
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
scenes = Scenes(pv_request)
|
||||||
|
scene_data: PowerviewData = await scenes.get_scenes()
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
shades = Shades(pv_request)
|
||||||
|
shade_data: PowerviewData = await shades.get_shades()
|
||||||
|
except HUB_EXCEPTIONS as err:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
f"Connection error to PowerView hub {hub_address}: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
if not device_info:
|
if not device_info:
|
||||||
raise ConfigEntryNotReady(f"Unable to initialize PowerView hub: {hub_address}")
|
raise ConfigEntryNotReady(f"Unable to initialize PowerView hub: {hub_address}")
|
||||||
|
|
||||||
coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub_address)
|
if CONF_API_VERSION not in config:
|
||||||
|
new_data = {**entry.data}
|
||||||
|
new_data[CONF_API_VERSION] = hub.api_version
|
||||||
|
hass.config_entries.async_update_entry(entry, data=new_data)
|
||||||
|
|
||||||
|
coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub)
|
||||||
coordinator.async_set_updated_data(PowerviewShadeData())
|
coordinator.async_set_updated_data(PowerviewShadeData())
|
||||||
# populate raw shade data into the coordinator for diagnostics
|
# populate raw shade data into the coordinator for diagnostics
|
||||||
coordinator.data.store_group_data(shade_entries[SHADE_DATA])
|
coordinator.data.store_group_data(shade_data)
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = PowerviewEntryData(
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = PowerviewEntryData(
|
||||||
api=pv_request,
|
api=pv_request,
|
||||||
room_data=room_data,
|
room_data=room_data.processed,
|
||||||
scene_data=scene_data,
|
scene_data=scene_data.processed,
|
||||||
shade_data=shade_data,
|
shade_data=shade_data.processed,
|
||||||
coordinator=coordinator,
|
coordinator=coordinator,
|
||||||
device_info=device_info,
|
device_info=device_info,
|
||||||
)
|
)
|
||||||
@ -107,39 +114,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_get_device_info(
|
async def async_get_device_info(hub: Hub) -> PowerviewDeviceInfo:
|
||||||
pv_request: AioRequest, hub_address: str
|
|
||||||
) -> PowerviewDeviceInfo:
|
|
||||||
"""Determine device info."""
|
"""Determine device info."""
|
||||||
userdata = UserData(pv_request)
|
|
||||||
resources = await userdata.get_resources()
|
|
||||||
userdata_data = resources[USER_DATA]
|
|
||||||
|
|
||||||
if FIRMWARE in userdata_data:
|
|
||||||
main_processor_info = userdata_data[FIRMWARE][FIRMWARE_MAINPROCESSOR]
|
|
||||||
elif userdata_data:
|
|
||||||
# Legacy devices
|
|
||||||
fwversion = ApiEntryPoint(pv_request, API_PATH_FWVERSION)
|
|
||||||
resources = await fwversion.get_resources()
|
|
||||||
|
|
||||||
if FIRMWARE in resources:
|
|
||||||
main_processor_info = resources[FIRMWARE][FIRMWARE_MAINPROCESSOR]
|
|
||||||
else:
|
|
||||||
main_processor_info = DEFAULT_LEGACY_MAINPROCESSOR
|
|
||||||
|
|
||||||
return PowerviewDeviceInfo(
|
return PowerviewDeviceInfo(
|
||||||
name=base64_to_unicode(userdata_data[HUB_NAME]),
|
name=hub.name,
|
||||||
mac_address=userdata_data[MAC_ADDRESS_IN_USERDATA],
|
mac_address=hub.mac_address,
|
||||||
serial_number=userdata_data[SERIAL_NUMBER_IN_USERDATA],
|
serial_number=hub.serial_number,
|
||||||
firmware=main_processor_info,
|
firmware=hub.firmware,
|
||||||
model=main_processor_info[FIRMWARE_NAME],
|
model=hub.model,
|
||||||
hub_address=hub_address,
|
hub_address=hub.ip,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@ -5,7 +5,14 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aiopvapi.resources.shade import BaseShade, factory as PvShade
|
from aiopvapi.helpers.constants import (
|
||||||
|
ATTR_NAME,
|
||||||
|
MOTION_CALIBRATE,
|
||||||
|
MOTION_FAVORITE,
|
||||||
|
MOTION_JOG,
|
||||||
|
)
|
||||||
|
from aiopvapi.hub import Hub
|
||||||
|
from aiopvapi.resources.shade import BaseShade
|
||||||
|
|
||||||
from homeassistant.components.button import (
|
from homeassistant.components.button import (
|
||||||
ButtonDeviceClass,
|
ButtonDeviceClass,
|
||||||
@ -17,7 +24,7 @@ from homeassistant.const import EntityCategory
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, ROOM_ID_IN_SHADE, ROOM_NAME_UNICODE
|
from .const import DOMAIN
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .entity import ShadeEntity
|
from .entity import ShadeEntity
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
@ -27,7 +34,8 @@ from .model import PowerviewDeviceInfo, PowerviewEntryData
|
|||||||
class PowerviewButtonDescriptionMixin:
|
class PowerviewButtonDescriptionMixin:
|
||||||
"""Mixin to describe a Button entity."""
|
"""Mixin to describe a Button entity."""
|
||||||
|
|
||||||
press_action: Callable[[BaseShade], Any]
|
press_action: Callable[[BaseShade | Hub], Any]
|
||||||
|
create_entity_fn: Callable[[BaseShade | Hub], bool]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -37,18 +45,20 @@ class PowerviewButtonDescription(
|
|||||||
"""Class to describe a Button entity."""
|
"""Class to describe a Button entity."""
|
||||||
|
|
||||||
|
|
||||||
BUTTONS: Final = [
|
BUTTONS_SHADE: Final = [
|
||||||
PowerviewButtonDescription(
|
PowerviewButtonDescription(
|
||||||
key="calibrate",
|
key="calibrate",
|
||||||
translation_key="calibrate",
|
translation_key="calibrate",
|
||||||
icon="mdi:swap-vertical-circle-outline",
|
icon="mdi:swap-vertical-circle-outline",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
create_entity_fn=lambda shade: shade.is_supported(MOTION_CALIBRATE),
|
||||||
press_action=lambda shade: shade.calibrate(),
|
press_action=lambda shade: shade.calibrate(),
|
||||||
),
|
),
|
||||||
PowerviewButtonDescription(
|
PowerviewButtonDescription(
|
||||||
key="identify",
|
key="identify",
|
||||||
device_class=ButtonDeviceClass.IDENTIFY,
|
device_class=ButtonDeviceClass.IDENTIFY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
create_entity_fn=lambda shade: shade.is_supported(MOTION_JOG),
|
||||||
press_action=lambda shade: shade.jog(),
|
press_action=lambda shade: shade.jog(),
|
||||||
),
|
),
|
||||||
PowerviewButtonDescription(
|
PowerviewButtonDescription(
|
||||||
@ -56,6 +66,7 @@ BUTTONS: Final = [
|
|||||||
translation_key="favorite",
|
translation_key="favorite",
|
||||||
icon="mdi:heart",
|
icon="mdi:heart",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
create_entity_fn=lambda shade: shade.is_supported(MOTION_FAVORITE),
|
||||||
press_action=lambda shade: shade.favorite(),
|
press_action=lambda shade: shade.favorite(),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -71,28 +82,25 @@ async def async_setup_entry(
|
|||||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
entities: list[ButtonEntity] = []
|
entities: list[ButtonEntity] = []
|
||||||
for raw_shade in pv_entry.shade_data.values():
|
for shade in pv_entry.shade_data.values():
|
||||||
shade: BaseShade = PvShade(raw_shade, pv_entry.api)
|
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")
|
||||||
name_before_refresh = shade.name
|
for description in BUTTONS_SHADE:
|
||||||
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
|
if description.create_entity_fn(shade):
|
||||||
room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
|
entities.append(
|
||||||
|
PowerviewShadeButton(
|
||||||
for description in BUTTONS:
|
pv_entry.coordinator,
|
||||||
entities.append(
|
pv_entry.device_info,
|
||||||
PowerviewButton(
|
room_name,
|
||||||
pv_entry.coordinator,
|
shade,
|
||||||
pv_entry.device_info,
|
shade.name,
|
||||||
room_name,
|
description,
|
||||||
shade,
|
)
|
||||||
name_before_refresh,
|
|
||||||
description,
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class PowerviewButton(ShadeEntity, ButtonEntity):
|
class PowerviewShadeButton(ShadeEntity, ButtonEntity):
|
||||||
"""Representation of an advanced feature button."""
|
"""Representation of an advanced feature button."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -3,14 +3,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from aiopvapi.helpers.aiorequest import AioRequest
|
from aiopvapi.helpers.aiorequest import AioRequest
|
||||||
|
from aiopvapi.hub import Hub
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core, exceptions
|
from homeassistant import config_entries, core, exceptions
|
||||||
from homeassistant.components import dhcp, zeroconf
|
from homeassistant.components import dhcp, zeroconf
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
@ -19,9 +20,9 @@ from .const import DOMAIN, HUB_EXCEPTIONS
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
|
|
||||||
HAP_SUFFIX = "._hap._tcp.local."
|
HAP_SUFFIX = "._hap._tcp.local."
|
||||||
POWERVIEW_SUFFIX = "._powerview._tcp.local."
|
POWERVIEW_G2_SUFFIX = "._powerview._tcp.local."
|
||||||
|
POWERVIEW_G3_SUFFIX = "._powerview-g3._tcp.local."
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str, str]:
|
async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str, str]:
|
||||||
@ -36,44 +37,70 @@ async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(10):
|
||||||
device_info = await async_get_device_info(pv_request, hub_address)
|
hub = Hub(pv_request)
|
||||||
|
await hub.query_firmware()
|
||||||
|
device_info = await async_get_device_info(hub)
|
||||||
except HUB_EXCEPTIONS as err:
|
except HUB_EXCEPTIONS as err:
|
||||||
raise CannotConnect from err
|
raise CannotConnect from err
|
||||||
|
|
||||||
|
if hub.role != "Primary":
|
||||||
|
raise UnsupportedDevice(
|
||||||
|
f"{hub.name} ({hub.hub_address}) is the {hub.role} Hub. "
|
||||||
|
"Only the Primary can manage shades"
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER.debug("Connection made using api version: %s", hub.api_version)
|
||||||
|
|
||||||
# Return info that you want to store in the config entry.
|
# Return info that you want to store in the config entry.
|
||||||
return {
|
return {
|
||||||
"title": device_info.name,
|
"title": device_info.name,
|
||||||
"unique_id": device_info.serial_number,
|
"unique_id": device_info.serial_number,
|
||||||
|
CONF_API_VERSION: hub.api_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class PowerviewConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Hunter Douglas PowerView."""
|
"""Handle a config flow for Hunter Douglas PowerView."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the powerview config flow."""
|
"""Initialize the powerview config flow."""
|
||||||
self.powerview_config: dict[str, str] = {}
|
self.powerview_config: dict = {}
|
||||||
self.discovered_ip: str | None = None
|
self.discovered_ip: str | None = None
|
||||||
self.discovered_name: str | None = None
|
self.discovered_name: str | None = None
|
||||||
|
self.data_schema: dict = {vol.Required(CONF_HOST): str}
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: dict[str, Any] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
info, error = await self._async_validate_or_error(user_input[CONF_HOST])
|
info, error = await self._async_validate_or_error(user_input[CONF_HOST])
|
||||||
|
|
||||||
if info and not error:
|
if info and not error:
|
||||||
|
self.powerview_config = {
|
||||||
|
CONF_HOST: user_input[CONF_HOST],
|
||||||
|
CONF_NAME: info["title"],
|
||||||
|
CONF_API_VERSION: info[CONF_API_VERSION],
|
||||||
|
}
|
||||||
await self.async_set_unique_id(info["unique_id"])
|
await self.async_set_unique_id(info["unique_id"])
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=info["title"], data={CONF_HOST: user_input[CONF_HOST]}
|
title=self.powerview_config[CONF_NAME],
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.powerview_config[CONF_HOST],
|
||||||
|
CONF_API_VERSION: self.powerview_config[CONF_API_VERSION],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert error is not None
|
||||||
errors["base"] = error
|
errors["base"] = error
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=vol.Schema(self.data_schema), errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_validate_or_error(
|
async def _async_validate_or_error(
|
||||||
@ -85,6 +112,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
info = await validate_input(self.hass, host)
|
info = await validate_input(self.hass, host)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
return None, "cannot_connect"
|
return None, "cannot_connect"
|
||||||
|
except UnsupportedDevice:
|
||||||
|
return None, "unsupported_device"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
return None, "unknown"
|
return None, "unknown"
|
||||||
@ -102,7 +131,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle zeroconf discovery."""
|
"""Handle zeroconf discovery."""
|
||||||
self.discovered_ip = discovery_info.host
|
self.discovered_ip = discovery_info.host
|
||||||
name = discovery_info.name.removesuffix(POWERVIEW_SUFFIX)
|
name = discovery_info.name.removesuffix(POWERVIEW_G2_SUFFIX)
|
||||||
|
name = name.removesuffix(POWERVIEW_G3_SUFFIX)
|
||||||
self.discovered_name = name
|
self.discovered_name = name
|
||||||
return await self.async_step_discovery_confirm()
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
@ -137,6 +167,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self.powerview_config = {
|
self.powerview_config = {
|
||||||
CONF_HOST: self.discovered_ip,
|
CONF_HOST: self.discovered_ip,
|
||||||
CONF_NAME: self.discovered_name,
|
CONF_NAME: self.discovered_name,
|
||||||
|
CONF_API_VERSION: info[CONF_API_VERSION],
|
||||||
}
|
}
|
||||||
return await self.async_step_link()
|
return await self.async_step_link()
|
||||||
|
|
||||||
@ -147,7 +178,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self.powerview_config[CONF_NAME],
|
title=self.powerview_config[CONF_NAME],
|
||||||
data={CONF_HOST: self.powerview_config[CONF_HOST]},
|
data={
|
||||||
|
CONF_HOST: self.powerview_config[CONF_HOST],
|
||||||
|
CONF_API_VERSION: self.powerview_config[CONF_API_VERSION],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._set_confirm_only()
|
self._set_confirm_only()
|
||||||
@ -159,3 +193,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
class CannotConnect(exceptions.HomeAssistantError):
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedDevice(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate the device is not supported."""
|
||||||
|
@ -1,91 +1,28 @@
|
|||||||
"""Support for Powerview scenes from a Powerview hub."""
|
"""Constants for Hunter Douglas Powerview hub."""
|
||||||
|
|
||||||
|
|
||||||
from aiohttp.client_exceptions import ServerDisconnectedError
|
from aiohttp.client_exceptions import ServerDisconnectedError
|
||||||
from aiopvapi.helpers.aiorequest import PvApiConnectionError, PvApiResponseStatusError
|
from aiopvapi.helpers.aiorequest import (
|
||||||
|
PvApiConnectionError,
|
||||||
|
PvApiEmptyData,
|
||||||
|
PvApiMaintenance,
|
||||||
|
PvApiResponseStatusError,
|
||||||
|
)
|
||||||
|
|
||||||
DOMAIN = "hunterdouglas_powerview"
|
DOMAIN = "hunterdouglas_powerview"
|
||||||
|
|
||||||
MANUFACTURER = "Hunter Douglas"
|
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"
|
|
||||||
HUB_NAME = "hubName"
|
|
||||||
|
|
||||||
FIRMWARE = "firmware"
|
|
||||||
FIRMWARE_MAINPROCESSOR = "mainProcessor"
|
|
||||||
FIRMWARE_NAME = "name"
|
|
||||||
FIRMWARE_REVISION = "revision"
|
|
||||||
FIRMWARE_SUB_REVISION = "subRevision"
|
|
||||||
FIRMWARE_BUILD = "build"
|
|
||||||
|
|
||||||
REDACT_MAC_ADDRESS = "mac_address"
|
REDACT_MAC_ADDRESS = "mac_address"
|
||||||
REDACT_SERIAL_NUMBER = "serial_number"
|
REDACT_SERIAL_NUMBER = "serial_number"
|
||||||
REDACT_HUB_ADDRESS = "hub_address"
|
REDACT_HUB_ADDRESS = "hub_address"
|
||||||
|
|
||||||
SCENE_NAME = "name"
|
STATE_ATTRIBUTE_ROOM_NAME = "room_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_BATTERY_LEVEL = "batteryStrength"
|
|
||||||
SHADE_BATTERY_LEVEL_MAX = 200
|
|
||||||
|
|
||||||
ATTR_SIGNAL_STRENGTH = "signalStrength"
|
|
||||||
ATTR_SIGNAL_STRENGTH_MAX = 4
|
|
||||||
|
|
||||||
STATE_ATTRIBUTE_ROOM_NAME = "roomName"
|
|
||||||
|
|
||||||
HUB_EXCEPTIONS = (
|
HUB_EXCEPTIONS = (
|
||||||
ServerDisconnectedError,
|
ServerDisconnectedError,
|
||||||
TimeoutError,
|
TimeoutError,
|
||||||
PvApiConnectionError,
|
PvApiConnectionError,
|
||||||
PvApiResponseStatusError,
|
PvApiResponseStatusError,
|
||||||
|
PvApiMaintenance,
|
||||||
|
PvApiEmptyData,
|
||||||
)
|
)
|
||||||
|
|
||||||
LEGACY_DEVICE_SUB_REVISION = 1
|
|
||||||
LEGACY_DEVICE_REVISION = 0
|
|
||||||
LEGACY_DEVICE_BUILD = 0
|
|
||||||
LEGACY_DEVICE_MODEL = "PowerView Hub"
|
|
||||||
|
|
||||||
DEFAULT_LEGACY_MAINPROCESSOR = {
|
|
||||||
FIRMWARE_REVISION: LEGACY_DEVICE_REVISION,
|
|
||||||
FIRMWARE_SUB_REVISION: LEGACY_DEVICE_SUB_REVISION,
|
|
||||||
FIRMWARE_BUILD: LEGACY_DEVICE_BUILD,
|
|
||||||
FIRMWARE_NAME: LEGACY_DEVICE_MODEL,
|
|
||||||
}
|
|
||||||
|
|
||||||
API_PATH_FWVERSION = "api/fwversion"
|
|
||||||
|
|
||||||
POS_KIND_NONE = 0
|
|
||||||
POS_KIND_PRIMARY = 1
|
|
||||||
POS_KIND_SECONDARY = 2
|
|
||||||
POS_KIND_VANE = 3
|
|
||||||
POS_KIND_ERROR = 4
|
|
||||||
|
|
||||||
|
|
||||||
ATTR_BATTERY_KIND = "batteryKind"
|
|
||||||
BATTERY_KIND_HARDWIRED = 1
|
|
||||||
BATTERY_KIND_BATTERY = 2
|
|
||||||
BATTERY_KIND_RECHARGABLE = 3
|
|
||||||
|
|
||||||
POWER_SUPPLY_TYPE_MAP = {
|
|
||||||
BATTERY_KIND_HARDWIRED: "Hardwired Power Supply",
|
|
||||||
BATTERY_KIND_BATTERY: "Battery Wand",
|
|
||||||
BATTERY_KIND_RECHARGABLE: "Rechargeable Battery",
|
|
||||||
}
|
|
||||||
POWER_SUPPLY_TYPE_REVERSE_MAP = {v: k for k, v in POWER_SUPPLY_TYPE_MAP.items()}
|
|
||||||
|
@ -5,12 +5,14 @@ import asyncio
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from aiopvapi.helpers.aiorequest import PvApiMaintenance
|
||||||
|
from aiopvapi.hub import Hub
|
||||||
from aiopvapi.shades import Shades
|
from aiopvapi.shades import Shades
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import SHADE_DATA
|
from .const import HUB_EXCEPTIONS
|
||||||
from .shade_data import PowerviewShadeData
|
from .shade_data import PowerviewShadeData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -19,18 +21,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]):
|
class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]):
|
||||||
"""DataUpdateCoordinator to gather data from a powerview hub."""
|
"""DataUpdateCoordinator to gather data from a powerview hub."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, shades: Shades, hub: Hub) -> None:
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
shades: Shades,
|
|
||||||
hub_address: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
|
"""Initialize DataUpdateCoordinator to gather data for specific Powerview Hub."""
|
||||||
self.shades = shades
|
self.shades = shades
|
||||||
|
self.hub = hub
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
name=f"powerview hub {hub_address}",
|
name=f"powerview hub {hub.hub_address}",
|
||||||
update_interval=timedelta(seconds=60),
|
update_interval=timedelta(seconds=60),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,17 +36,20 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData])
|
|||||||
"""Fetch data from shade endpoint."""
|
"""Fetch data from shade endpoint."""
|
||||||
|
|
||||||
async with asyncio.timeout(10):
|
async with asyncio.timeout(10):
|
||||||
shade_entries = await self.shades.get_resources()
|
try:
|
||||||
|
shade_entries = await self.shades.get_shades()
|
||||||
if isinstance(shade_entries, bool):
|
except PvApiMaintenance as error:
|
||||||
# hub returns boolean on a 204/423 empty response (maintenance)
|
# hub is undergoing maintenance, pause polling
|
||||||
# continual polling results in inevitable error
|
raise UpdateFailed(error) from error
|
||||||
raise UpdateFailed("Powerview Hub is undergoing maintenance")
|
except HUB_EXCEPTIONS as error:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Powerview Hub {self.hub.hub_address} did not return any data: {error}"
|
||||||
|
) from error
|
||||||
|
|
||||||
if not shade_entries:
|
if not shade_entries:
|
||||||
raise UpdateFailed("Failed to fetch new shade data")
|
raise UpdateFailed("No new shade data was returned")
|
||||||
|
|
||||||
# only update if shade_entries is valid
|
# only update if shade_entries is valid
|
||||||
self.data.store_group_data(shade_entries[SHADE_DATA])
|
self.data.store_group_data(shade_entries)
|
||||||
|
|
||||||
return self.data
|
return self.data
|
||||||
|
@ -4,21 +4,20 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable, Iterable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import replace
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiopvapi.helpers.constants import (
|
from aiopvapi.helpers.constants import (
|
||||||
ATTR_POSITION1,
|
ATTR_NAME,
|
||||||
ATTR_POSITION2,
|
CLOSED_POSITION,
|
||||||
ATTR_POSITION_DATA,
|
|
||||||
ATTR_POSKIND1,
|
|
||||||
ATTR_POSKIND2,
|
|
||||||
MAX_POSITION,
|
MAX_POSITION,
|
||||||
MIN_POSITION,
|
MIN_POSITION,
|
||||||
|
MOTION_STOP,
|
||||||
)
|
)
|
||||||
from aiopvapi.resources.shade import BaseShade, factory as PvShade
|
from aiopvapi.resources.shade import BaseShade, ShadePosition
|
||||||
|
|
||||||
from homeassistant.components.cover import (
|
from homeassistant.components.cover import (
|
||||||
ATTR_POSITION,
|
ATTR_POSITION,
|
||||||
@ -32,20 +31,10 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN, STATE_ATTRIBUTE_ROOM_NAME
|
||||||
DOMAIN,
|
|
||||||
LEGACY_DEVICE_MODEL,
|
|
||||||
POS_KIND_PRIMARY,
|
|
||||||
POS_KIND_SECONDARY,
|
|
||||||
POS_KIND_VANE,
|
|
||||||
ROOM_ID_IN_SHADE,
|
|
||||||
ROOM_NAME_UNICODE,
|
|
||||||
STATE_ATTRIBUTE_ROOM_NAME,
|
|
||||||
)
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .entity import ShadeEntity
|
from .entity import ShadeEntity
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
from .shade_data import PowerviewShadeMove
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -57,14 +46,6 @@ PARALLEL_UPDATES = 1
|
|||||||
|
|
||||||
RESYNC_DELAY = 60
|
RESYNC_DELAY = 60
|
||||||
|
|
||||||
# this equates to 0.75/100 in terms of hass blind position
|
|
||||||
# some blinds in a closed position report less than 655.35 (1%)
|
|
||||||
# but larger than 0 even though they are clearly closed
|
|
||||||
# Find 1 percent of MAX_POSITION, then find 75% of that number
|
|
||||||
# The means currently 491.5125 or less is closed position
|
|
||||||
# implemented for top/down shades, but also works fine with normal shades
|
|
||||||
CLOSED_POSITION = (0.75 / 100) * (MAX_POSITION - MIN_POSITION)
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=10)
|
SCAN_INTERVAL = timedelta(minutes=10)
|
||||||
|
|
||||||
|
|
||||||
@ -77,42 +58,23 @@ async def async_setup_entry(
|
|||||||
coordinator: PowerviewShadeUpdateCoordinator = pv_entry.coordinator
|
coordinator: PowerviewShadeUpdateCoordinator = pv_entry.coordinator
|
||||||
|
|
||||||
entities: list[ShadeEntity] = []
|
entities: list[ShadeEntity] = []
|
||||||
for raw_shade in pv_entry.shade_data.values():
|
for shade in pv_entry.shade_data.values():
|
||||||
# The shade may be out of sync with the hub
|
# The shade may be out of sync with the hub
|
||||||
# so we force a refresh when we add it if possible
|
# so we force a refresh when we add it if possible
|
||||||
shade: BaseShade = PvShade(raw_shade, pv_entry.api)
|
|
||||||
name_before_refresh = shade.name
|
|
||||||
with suppress(TimeoutError):
|
with suppress(TimeoutError):
|
||||||
async with asyncio.timeout(1):
|
async with asyncio.timeout(1):
|
||||||
await shade.refresh()
|
await shade.refresh()
|
||||||
|
coordinator.data.update_shade_position(shade.id, shade.current_position)
|
||||||
if ATTR_POSITION_DATA not in shade.raw_data:
|
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")
|
||||||
_LOGGER.info(
|
|
||||||
"The %s shade was skipped because it is missing position data",
|
|
||||||
name_before_refresh,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
coordinator.data.update_shade_positions(shade.raw_data)
|
|
||||||
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
|
|
||||||
room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
|
|
||||||
entities.extend(
|
entities.extend(
|
||||||
create_powerview_shade_entity(
|
create_powerview_shade_entity(
|
||||||
coordinator, pv_entry.device_info, room_name, shade, name_before_refresh
|
coordinator, pv_entry.device_info, room_name, shade, shade.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
def hd_position_to_hass(hd_position: int, max_val: int = MAX_POSITION) -> int:
|
|
||||||
"""Convert hunter douglas position to hass position."""
|
|
||||||
return round((hd_position / max_val) * 100)
|
|
||||||
|
|
||||||
|
|
||||||
def hass_position_to_hd(hass_position: int, max_val: int = MAX_POSITION) -> int:
|
|
||||||
"""Convert hass position to hunter douglas position."""
|
|
||||||
return int(hass_position / 100 * max_val)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
||||||
"""Representation of a powerview shade."""
|
"""Representation of a powerview shade."""
|
||||||
|
|
||||||
@ -135,7 +97,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
super().__init__(coordinator, device_info, room_name, shade, name)
|
super().__init__(coordinator, device_info, room_name, shade, name)
|
||||||
self._shade: BaseShade = shade
|
self._shade: BaseShade = shade
|
||||||
self._scheduled_transition_update: CALLBACK_TYPE | None = None
|
self._scheduled_transition_update: CALLBACK_TYPE | None = None
|
||||||
if self._device_info.model != LEGACY_DEVICE_MODEL:
|
if self._shade.is_supported(MOTION_STOP):
|
||||||
self._attr_supported_features |= CoverEntityFeature.STOP
|
self._attr_supported_features |= CoverEntityFeature.STOP
|
||||||
self._forced_resync: Callable[[], None] | None = None
|
self._forced_resync: Callable[[], None] | None = None
|
||||||
|
|
||||||
@ -172,22 +134,22 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
@property
|
@property
|
||||||
def current_cover_position(self) -> int:
|
def current_cover_position(self) -> int:
|
||||||
"""Return the current position of cover."""
|
"""Return the current position of cover."""
|
||||||
return hd_position_to_hass(self.positions.primary, MAX_POSITION)
|
return self.positions.primary
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transition_steps(self) -> int:
|
def transition_steps(self) -> int:
|
||||||
"""Return the steps to make a move."""
|
"""Return the steps to make a move."""
|
||||||
return hd_position_to_hass(self.positions.primary, MAX_POSITION)
|
return self.positions.primary
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_position(self) -> PowerviewShadeMove:
|
def open_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
return PowerviewShadeMove(self._shade.open_position, {})
|
return replace(self._shade.open_position, velocity=self.positions.velocity)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_position(self) -> PowerviewShadeMove:
|
def close_position(self) -> ShadePosition:
|
||||||
"""Return the close position and required additional positions."""
|
"""Return the close position and required additional positions."""
|
||||||
return PowerviewShadeMove(self._shade.close_position, {})
|
return replace(self._shade.close_position, velocity=self.positions.velocity)
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||||
"""Close the cover."""
|
"""Close the cover."""
|
||||||
@ -208,12 +170,12 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||||
"""Stop the cover."""
|
"""Stop the cover."""
|
||||||
self._async_cancel_scheduled_transition_update()
|
self._async_cancel_scheduled_transition_update()
|
||||||
self.data.update_from_response(await self._shade.stop())
|
await self._shade.stop()
|
||||||
await self._async_force_refresh_state()
|
await self._async_force_refresh_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
||||||
"""Dont allow a cover to go into an impossbile position."""
|
"""Don't allow a cover to go into an impossbile position."""
|
||||||
# no override required in base
|
# no override required in base
|
||||||
return target_hass_position
|
return target_hass_position
|
||||||
|
|
||||||
@ -222,21 +184,21 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
await self._async_set_cover_position(kwargs[ATTR_POSITION])
|
await self._async_set_cover_position(kwargs[ATTR_POSITION])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_one = hass_position_to_hd(target_hass_position)
|
"""Return a ShadePosition."""
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{ATTR_POSITION1: position_one, ATTR_POSKIND1: POS_KIND_PRIMARY}, {}
|
primary=target_hass_position,
|
||||||
|
velocity=self.positions.velocity,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_execute_move(self, move: PowerviewShadeMove) -> None:
|
async def _async_execute_move(self, move: ShadePosition) -> None:
|
||||||
"""Execute a move that can affect multiple positions."""
|
"""Execute a move that can affect multiple positions."""
|
||||||
response = await self._shade.move(move.request)
|
_LOGGER.debug("Move request %s: %s", self.name, move)
|
||||||
# Process any positions we know will update as result
|
response = await self._shade.move(move)
|
||||||
# of the request since the hub won't return them
|
_LOGGER.debug("Move response %s: %s", self.name, response)
|
||||||
for kind, position in move.new_positions.items():
|
|
||||||
self.data.update_shade_position(self._shade.id, position, kind)
|
# Process the response from the hub (including new positions)
|
||||||
# Finally process the response
|
self.data.update_shade_position(self._shade.id, response)
|
||||||
self.data.update_from_response(response)
|
|
||||||
|
|
||||||
async def _async_set_cover_position(self, target_hass_position: int) -> None:
|
async def _async_set_cover_position(self, target_hass_position: int) -> None:
|
||||||
"""Move the shade to a position."""
|
"""Move the shade to a position."""
|
||||||
@ -251,9 +213,9 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_update_shade_data(self, shade_data: dict[str | int, Any]) -> None:
|
def _async_update_shade_data(self, shade_data: ShadePosition) -> None:
|
||||||
"""Update the current cover position from the data."""
|
"""Update the current cover position from the data."""
|
||||||
self.data.update_shade_positions(shade_data)
|
self.data.update_shade_position(self._shade.id, shade_data)
|
||||||
self._attr_is_opening = False
|
self._attr_is_opening = False
|
||||||
self._attr_is_closing = False
|
self._attr_is_closing = False
|
||||||
|
|
||||||
@ -283,7 +245,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
est_time_to_complete_transition,
|
est_time_to_complete_transition,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Schedule an forced update for when we expect the transition
|
# Schedule a forced update for when we expect the transition
|
||||||
# to be completed.
|
# to be completed.
|
||||||
self._scheduled_transition_update = async_call_later(
|
self._scheduled_transition_update = async_call_later(
|
||||||
self.hass,
|
self.hass,
|
||||||
@ -342,8 +304,12 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
|
|||||||
# The update will likely timeout and
|
# The update will likely timeout and
|
||||||
# error if are already have one in flight
|
# error if are already have one in flight
|
||||||
return
|
return
|
||||||
await self._shade.refresh()
|
# suppress timeouts caused by hub nightly reboot
|
||||||
self._async_update_shade_data(self._shade.raw_data)
|
with suppress(asyncio.TimeoutError):
|
||||||
|
async with asyncio.timeout(5):
|
||||||
|
await self._shade.refresh()
|
||||||
|
_LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position)
|
||||||
|
self._async_update_shade_data(self._shade.current_position)
|
||||||
|
|
||||||
|
|
||||||
class PowerViewShade(PowerViewShadeBase):
|
class PowerViewShade(PowerViewShadeBase):
|
||||||
@ -372,31 +338,31 @@ class PowerViewShadeWithTiltBase(PowerViewShadeBase):
|
|||||||
| CoverEntityFeature.CLOSE_TILT
|
| CoverEntityFeature.CLOSE_TILT
|
||||||
| CoverEntityFeature.SET_TILT_POSITION
|
| CoverEntityFeature.SET_TILT_POSITION
|
||||||
)
|
)
|
||||||
if self._device_info.model != LEGACY_DEVICE_MODEL:
|
if self._shade.is_supported(MOTION_STOP):
|
||||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_cover_tilt_position(self) -> int:
|
def current_cover_tilt_position(self) -> int:
|
||||||
"""Return the current cover tile position."""
|
"""Return the current cover tile position."""
|
||||||
return hd_position_to_hass(self.positions.vane, self._max_tilt)
|
return self.positions.tilt
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transition_steps(self) -> int:
|
def transition_steps(self) -> int:
|
||||||
"""Return the steps to make a move."""
|
"""Return the steps to make a move."""
|
||||||
return hd_position_to_hass(
|
return self.positions.primary + self.positions.tilt
|
||||||
self.positions.primary, MAX_POSITION
|
|
||||||
) + hd_position_to_hass(self.positions.vane, self._max_tilt)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_tilt_position(self) -> PowerviewShadeMove:
|
def open_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the open tilt position and required additional positions."""
|
"""Return the open tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(self._shade.open_position_tilt, {})
|
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_tilt_position(self) -> PowerviewShadeMove:
|
def close_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the close tilt position and required additional positions."""
|
"""Return the close tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(self._shade.close_position_tilt, {})
|
return replace(
|
||||||
|
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||||
|
)
|
||||||
|
|
||||||
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
|
||||||
"""Close the cover tilt."""
|
"""Close the cover tilt."""
|
||||||
@ -411,13 +377,13 @@ class PowerViewShadeWithTiltBase(PowerViewShadeBase):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||||
"""Move the vane to a specific position."""
|
"""Move the tilt to a specific position."""
|
||||||
await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION])
|
await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION])
|
||||||
|
|
||||||
async def _async_set_cover_tilt_position(
|
async def _async_set_cover_tilt_position(
|
||||||
self, target_hass_tilt_position: int
|
self, target_hass_tilt_position: int
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Move the vane to a specific position."""
|
"""Move the tilt to a specific position."""
|
||||||
final_position = self.current_cover_position + target_hass_tilt_position
|
final_position = self.current_cover_position + target_hass_tilt_position
|
||||||
self._async_schedule_update_for_transition(
|
self._async_schedule_update_for_transition(
|
||||||
abs(self.transition_steps - final_position)
|
abs(self.transition_steps - final_position)
|
||||||
@ -426,11 +392,19 @@ class PowerViewShadeWithTiltBase(PowerViewShadeBase):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
"""Return a PowerviewShadeMove."""
|
"""Return a ShadePosition."""
|
||||||
position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt)
|
return ShadePosition(
|
||||||
return PowerviewShadeMove(
|
primary=target_hass_position,
|
||||||
{ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE}, {}
|
velocity=self.positions.velocity,
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||||
|
"""Return a ShadePosition."""
|
||||||
|
return ShadePosition(
|
||||||
|
tilt=target_hass_tilt_position,
|
||||||
|
velocity=self.positions.velocity,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
|
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
|
||||||
@ -450,49 +424,25 @@ class PowerViewShadeWithTiltOnClosed(PowerViewShadeWithTiltBase):
|
|||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_position(self) -> PowerviewShadeMove:
|
def open_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(self._shade.open_position, velocity=self.positions.velocity)
|
||||||
self._shade.open_position, {POS_KIND_VANE: MIN_POSITION}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_position(self) -> PowerviewShadeMove:
|
def close_position(self) -> ShadePosition:
|
||||||
"""Return the close position and required additional positions."""
|
"""Return the close position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(self._shade.close_position, velocity=self.positions.velocity)
|
||||||
self._shade.close_position, {POS_KIND_VANE: MIN_POSITION}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_tilt_position(self) -> PowerviewShadeMove:
|
def open_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the open tilt position and required additional positions."""
|
"""Return the open tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||||
self._shade.open_position_tilt, {POS_KIND_PRIMARY: MIN_POSITION}
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_tilt_position(self) -> PowerviewShadeMove:
|
def close_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the close tilt position and required additional positions."""
|
"""Return the close tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(
|
||||||
self._shade.close_position_tilt, {POS_KIND_PRIMARY: MIN_POSITION}
|
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
|
||||||
"""Return a PowerviewShadeMove."""
|
|
||||||
position_shade = hass_position_to_hd(target_hass_position)
|
|
||||||
return PowerviewShadeMove(
|
|
||||||
{ATTR_POSITION1: position_shade, ATTR_POSKIND1: POS_KIND_PRIMARY},
|
|
||||||
{POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove:
|
|
||||||
"""Return a PowerviewShadeMove."""
|
|
||||||
position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt)
|
|
||||||
return PowerviewShadeMove(
|
|
||||||
{ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -506,32 +456,21 @@ class PowerViewShadeWithTiltAnywhere(PowerViewShadeWithTiltBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION)
|
"""Return a ShadePosition."""
|
||||||
position_vane = self.positions.vane
|
return ShadePosition(
|
||||||
return PowerviewShadeMove(
|
primary=target_hass_position,
|
||||||
{
|
tilt=self.positions.tilt,
|
||||||
ATTR_POSITION1: position_shade,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSITION2: position_vane,
|
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
ATTR_POSKIND2: POS_KIND_VANE,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove:
|
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||||
"""Return a PowerviewShadeMove."""
|
"""Return a ShadePosition."""
|
||||||
position_shade = self.positions.primary
|
return ShadePosition(
|
||||||
position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt)
|
primary=self.positions.primary,
|
||||||
return PowerviewShadeMove(
|
tilt=target_hass_tilt_position,
|
||||||
{
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSITION1: position_shade,
|
|
||||||
ATTR_POSITION2: position_vane,
|
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
ATTR_POSKIND2: POS_KIND_VANE,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -558,7 +497,7 @@ class PowerViewShadeTiltOnly(PowerViewShadeWithTiltBase):
|
|||||||
| CoverEntityFeature.CLOSE_TILT
|
| CoverEntityFeature.CLOSE_TILT
|
||||||
| CoverEntityFeature.SET_TILT_POSITION
|
| CoverEntityFeature.SET_TILT_POSITION
|
||||||
)
|
)
|
||||||
if self._device_info.model != LEGACY_DEVICE_MODEL:
|
if self._shade.is_supported(MOTION_STOP):
|
||||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||||
|
|
||||||
@ -577,17 +516,18 @@ class PowerViewShadeTopDown(PowerViewShadeBase):
|
|||||||
@property
|
@property
|
||||||
def current_cover_position(self) -> int:
|
def current_cover_position(self) -> int:
|
||||||
"""Return the current position of cover."""
|
"""Return the current position of cover."""
|
||||||
return hd_position_to_hass(MAX_POSITION - self.positions.primary, MAX_POSITION)
|
# inverted positioning
|
||||||
|
return MAX_POSITION - self.positions.primary
|
||||||
|
|
||||||
|
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||||
|
"""Move the shade to a specific position."""
|
||||||
|
await self._async_set_cover_position(MAX_POSITION - kwargs[ATTR_POSITION])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool:
|
def is_closed(self) -> bool:
|
||||||
"""Return if the cover is closed."""
|
"""Return if the cover is closed."""
|
||||||
return (MAX_POSITION - self.positions.primary) <= CLOSED_POSITION
|
return (MAX_POSITION - self.positions.primary) <= CLOSED_POSITION
|
||||||
|
|
||||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
|
||||||
"""Move the shade to a specific position."""
|
|
||||||
await self._async_set_cover_position(100 - kwargs[ATTR_POSITION])
|
|
||||||
|
|
||||||
|
|
||||||
class PowerViewShadeDualRailBase(PowerViewShadeBase):
|
class PowerViewShadeDualRailBase(PowerViewShadeBase):
|
||||||
"""Representation of a shade with top/down bottom/up capabilities.
|
"""Representation of a shade with top/down bottom/up capabilities.
|
||||||
@ -600,9 +540,7 @@ class PowerViewShadeDualRailBase(PowerViewShadeBase):
|
|||||||
@property
|
@property
|
||||||
def transition_steps(self) -> int:
|
def transition_steps(self) -> int:
|
||||||
"""Return the steps to make a move."""
|
"""Return the steps to make a move."""
|
||||||
return hd_position_to_hass(
|
return self.positions.primary + self.positions.secondary
|
||||||
self.positions.primary, MAX_POSITION
|
|
||||||
) + hd_position_to_hass(self.positions.secondary, MAX_POSITION)
|
|
||||||
|
|
||||||
|
|
||||||
class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase):
|
class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase):
|
||||||
@ -629,22 +567,16 @@ class PowerViewShadeTDBUBottom(PowerViewShadeDualRailBase):
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
||||||
"""Dont allow a cover to go into an impossbile position."""
|
"""Don't allow a cover to go into an impossbile position."""
|
||||||
cover_top = hd_position_to_hass(self.positions.secondary, MAX_POSITION)
|
return min(target_hass_position, (MAX_POSITION - self.positions.secondary))
|
||||||
return min(target_hass_position, (100 - cover_top))
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_bottom = hass_position_to_hd(target_hass_position)
|
"""Return a ShadePosition."""
|
||||||
position_top = self.positions.secondary
|
return ShadePosition(
|
||||||
return PowerviewShadeMove(
|
primary=target_hass_position,
|
||||||
{
|
secondary=self.positions.secondary,
|
||||||
ATTR_POSITION1: position_bottom,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSITION2: position_top,
|
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
ATTR_POSKIND2: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -689,41 +621,31 @@ class PowerViewShadeTDBUTop(PowerViewShadeDualRailBase):
|
|||||||
def current_cover_position(self) -> int:
|
def current_cover_position(self) -> int:
|
||||||
"""Return the current position of cover."""
|
"""Return the current position of cover."""
|
||||||
# these need to be inverted to report state correctly in HA
|
# these need to be inverted to report state correctly in HA
|
||||||
return hd_position_to_hass(self.positions.secondary, MAX_POSITION)
|
return self.positions.secondary
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_position(self) -> PowerviewShadeMove:
|
def open_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
# these shades share a class in parent API
|
# these shades share a class in parent API
|
||||||
# override open position for top shade
|
# override open position for top shade
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
primary=MIN_POSITION,
|
||||||
ATTR_POSITION1: MIN_POSITION,
|
secondary=MAX_POSITION,
|
||||||
ATTR_POSITION2: MAX_POSITION,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
ATTR_POSKIND2: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
def _clamp_cover_limit(self, target_hass_position: int) -> int:
|
||||||
"""Don't allow a cover to go into an impossbile position."""
|
"""Don't allow a cover to go into an impossbile position."""
|
||||||
cover_bottom = hd_position_to_hass(self.positions.primary, MAX_POSITION)
|
return min(target_hass_position, (MAX_POSITION - self.positions.primary))
|
||||||
return min(target_hass_position, (100 - cover_bottom))
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_bottom = self.positions.primary
|
"""Return a ShadePosition."""
|
||||||
position_top = hass_position_to_hd(target_hass_position, MAX_POSITION)
|
return ShadePosition(
|
||||||
return PowerviewShadeMove(
|
primary=self.positions.primary,
|
||||||
{
|
secondary=target_hass_position,
|
||||||
ATTR_POSITION1: position_bottom,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSITION2: position_top,
|
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
ATTR_POSKIND2: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -739,33 +661,27 @@ class PowerViewShadeDualOverlappedBase(PowerViewShadeBase):
|
|||||||
# poskind 1 represents the second half of the shade in hass
|
# poskind 1 represents the second half of the shade in hass
|
||||||
# front must be fully closed before rear can move
|
# front must be fully closed before rear can move
|
||||||
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
||||||
primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50
|
primary = (self.positions.primary / 2) + 50
|
||||||
# poskind 2 represents the shade first half of the shade in hass
|
# poskind 2 represents the shade first half of the shade in hass
|
||||||
# rear (opaque) must be fully open before front can move
|
# rear (opaque) must be fully open before front can move
|
||||||
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
||||||
secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2
|
secondary = self.positions.secondary / 2
|
||||||
return ceil(primary + secondary)
|
return ceil(primary + secondary)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_position(self) -> PowerviewShadeMove:
|
def open_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
primary=MAX_POSITION,
|
||||||
ATTR_POSITION1: MAX_POSITION,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_SECONDARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_position(self) -> PowerviewShadeMove:
|
def close_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
secondary=MIN_POSITION,
|
||||||
ATTR_POSITION1: MIN_POSITION,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -782,7 +698,6 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase):
|
|||||||
|
|
||||||
_attr_translation_key = "combined"
|
_attr_translation_key = "combined"
|
||||||
|
|
||||||
# type
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: PowerviewShadeUpdateCoordinator,
|
coordinator: PowerviewShadeUpdateCoordinator,
|
||||||
@ -806,36 +721,28 @@ class PowerViewShadeDualOverlappedCombined(PowerViewShadeDualOverlappedBase):
|
|||||||
"""Return the current position of cover."""
|
"""Return the current position of cover."""
|
||||||
# if front is open return that (other positions are impossible)
|
# if front is open return that (other positions are impossible)
|
||||||
# if front shade is closed get position of rear
|
# if front shade is closed get position of rear
|
||||||
position = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50
|
position = (self.positions.primary / 2) + 50
|
||||||
if self.positions.primary == MIN_POSITION:
|
if self.positions.primary == MIN_POSITION:
|
||||||
position = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2
|
position = self.positions.secondary / 2
|
||||||
|
|
||||||
return ceil(position)
|
return ceil(position)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION)
|
"""Return a ShadePosition."""
|
||||||
# note we set POS_KIND_VANE: MIN_POSITION here even with shades without
|
# 0 - 50 represents the rear blockut shade
|
||||||
# tilt so no additional override is required for differences between type 8/9/10
|
|
||||||
# this just stores the value in the coordinator for future reference
|
|
||||||
if target_hass_position <= 50:
|
if target_hass_position <= 50:
|
||||||
target_hass_position = target_hass_position * 2
|
target_hass_position = target_hass_position * 2
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
secondary=target_hass_position,
|
||||||
ATTR_POSITION1: position_shade,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 51 <= target_hass_position <= 100 (51-100 represents front sheer shade)
|
# 51 <= target_hass_position <= 100 (51-100 represents front sheer shade)
|
||||||
target_hass_position = (target_hass_position - 50) * 2
|
target_hass_position = (target_hass_position - 50) * 2
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
primary=target_hass_position,
|
||||||
ATTR_POSITION1: position_shade,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -879,28 +786,19 @@ class PowerViewShadeDualOverlappedFront(PowerViewShadeDualOverlappedBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION)
|
"""Return a ShadePosition."""
|
||||||
# note we set POS_KIND_VANE: MIN_POSITION here even with shades without tilt so no additional
|
return ShadePosition(
|
||||||
# override is required for differences between type 8/9/10
|
primary=target_hass_position,
|
||||||
# this just stores the value in the coordinator for future reference
|
velocity=self.positions.velocity,
|
||||||
return PowerviewShadeMove(
|
|
||||||
{
|
|
||||||
ATTR_POSITION1: position_shade,
|
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_position(self) -> PowerviewShadeMove:
|
def close_position(self) -> ShadePosition:
|
||||||
"""Return the close position and required additional positions."""
|
"""Return the close position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
primary=MIN_POSITION,
|
||||||
ATTR_POSITION1: MIN_POSITION,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_PRIMARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_SECONDARY: MAX_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -952,31 +850,22 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
|
|||||||
@property
|
@property
|
||||||
def current_cover_position(self) -> int:
|
def current_cover_position(self) -> int:
|
||||||
"""Return the current position of cover."""
|
"""Return the current position of cover."""
|
||||||
return hd_position_to_hass(self.positions.secondary, MAX_POSITION)
|
return self.positions.secondary
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
|
def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
|
||||||
position_shade = hass_position_to_hd(target_hass_position, MAX_POSITION)
|
"""Return a ShadePosition."""
|
||||||
# note we set POS_KIND_VANE: MIN_POSITION here even with shades without tilt so no additional
|
return ShadePosition(
|
||||||
# override is required for differences between type 8/9/10
|
secondary=target_hass_position,
|
||||||
# this just stores the value in the coordinator for future reference
|
velocity=self.positions.velocity,
|
||||||
return PowerviewShadeMove(
|
|
||||||
{
|
|
||||||
ATTR_POSITION1: position_shade,
|
|
||||||
ATTR_POSKIND1: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_position(self) -> PowerviewShadeMove:
|
def open_position(self) -> ShadePosition:
|
||||||
"""Return the open position and required additional positions."""
|
"""Return the open position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return ShadePosition(
|
||||||
{
|
secondary=MAX_POSITION,
|
||||||
ATTR_POSITION1: MAX_POSITION,
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSKIND1: POS_KIND_SECONDARY,
|
|
||||||
},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_VANE: MIN_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1010,7 +899,7 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
|||||||
| CoverEntityFeature.CLOSE_TILT
|
| CoverEntityFeature.CLOSE_TILT
|
||||||
| CoverEntityFeature.SET_TILT_POSITION
|
| CoverEntityFeature.SET_TILT_POSITION
|
||||||
)
|
)
|
||||||
if self._device_info.model != LEGACY_DEVICE_MODEL:
|
if self._shade.is_supported(MOTION_STOP):
|
||||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||||
|
|
||||||
@ -1020,40 +909,32 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
|||||||
# poskind 1 represents the second half of the shade in hass
|
# poskind 1 represents the second half of the shade in hass
|
||||||
# front must be fully closed before rear can move
|
# front must be fully closed before rear can move
|
||||||
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
||||||
primary = (hd_position_to_hass(self.positions.primary, MAX_POSITION) / 2) + 50
|
primary = (self.positions.primary / 2) + 50
|
||||||
# poskind 2 represents the shade first half of the shade in hass
|
# poskind 2 represents the shade first half of the shade in hass
|
||||||
# rear (opaque) must be fully open before front can move
|
# rear (opaque) must be fully open before front can move
|
||||||
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
# 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
|
||||||
secondary = hd_position_to_hass(self.positions.secondary, MAX_POSITION) / 2
|
secondary = self.positions.secondary / 2
|
||||||
vane = hd_position_to_hass(self.positions.vane, self._max_tilt)
|
tilt = self.positions.tilt
|
||||||
return ceil(primary + secondary + vane)
|
return ceil(primary + secondary + tilt)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove:
|
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||||
"""Return a PowerviewShadeMove."""
|
"""Return a ShadePosition."""
|
||||||
position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt)
|
return ShadePosition(
|
||||||
return PowerviewShadeMove(
|
tilt=target_hass_tilt_position,
|
||||||
{
|
velocity=self.positions.velocity,
|
||||||
ATTR_POSITION1: position_vane,
|
|
||||||
ATTR_POSKIND1: POS_KIND_VANE,
|
|
||||||
},
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def open_tilt_position(self) -> PowerviewShadeMove:
|
def open_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the open tilt position and required additional positions."""
|
"""Return the open tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||||
self._shade.open_position_tilt,
|
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION},
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def close_tilt_position(self) -> PowerviewShadeMove:
|
def close_tilt_position(self) -> ShadePosition:
|
||||||
"""Return the open tilt position and required additional positions."""
|
"""Return the open tilt position and required additional positions."""
|
||||||
return PowerviewShadeMove(
|
return replace(
|
||||||
self._shade.open_position_tilt,
|
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||||
{POS_KIND_PRIMARY: MIN_POSITION, POS_KIND_SECONDARY: MAX_POSITION},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1099,7 +980,8 @@ def create_powerview_shade_entity(
|
|||||||
shade.capability.type, (PowerViewShade,)
|
shade.capability.type, (PowerViewShade,)
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s (%s) detected as %a %s",
|
"%s %s (%s) detected as %a %s",
|
||||||
|
room_name,
|
||||||
shade.name,
|
shade.name,
|
||||||
shade.capability.type,
|
shade.capability.type,
|
||||||
classes,
|
classes,
|
||||||
|
@ -1,25 +1,19 @@
|
|||||||
"""The powerview integration base entity."""
|
"""The powerview integration base entity."""
|
||||||
|
|
||||||
from aiopvapi.resources.shade import ATTR_TYPE, BaseShade
|
import logging
|
||||||
|
|
||||||
|
from aiopvapi.resources.shade import BaseShade, ShadePosition
|
||||||
|
|
||||||
from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION
|
|
||||||
import homeassistant.helpers.device_registry as dr
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN, MANUFACTURER
|
||||||
ATTR_BATTERY_KIND,
|
|
||||||
BATTERY_KIND_HARDWIRED,
|
|
||||||
DOMAIN,
|
|
||||||
FIRMWARE,
|
|
||||||
FIRMWARE_BUILD,
|
|
||||||
FIRMWARE_REVISION,
|
|
||||||
FIRMWARE_SUB_REVISION,
|
|
||||||
MANUFACTURER,
|
|
||||||
)
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .model import PowerviewDeviceInfo
|
from .model import PowerviewDeviceInfo
|
||||||
from .shade_data import PowerviewShadeData, PowerviewShadePositions
|
from .shade_data import PowerviewShadeData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]):
|
class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]):
|
||||||
@ -39,6 +33,7 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]):
|
|||||||
self._room_name = room_name
|
self._room_name = room_name
|
||||||
self._attr_unique_id = unique_id
|
self._attr_unique_id = unique_id
|
||||||
self._device_info = device_info
|
self._device_info = device_info
|
||||||
|
self._configuration_url = self.coordinator.hub.url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> PowerviewShadeData:
|
def data(self) -> PowerviewShadeData:
|
||||||
@ -48,17 +43,14 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]):
|
|||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return the device_info of the device."""
|
"""Return the device_info of the device."""
|
||||||
firmware = self._device_info.firmware
|
|
||||||
sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}"
|
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)},
|
connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)},
|
||||||
identifiers={(DOMAIN, self._device_info.serial_number)},
|
identifiers={(DOMAIN, self._device_info.serial_number)},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=self._device_info.model,
|
model=self._device_info.model,
|
||||||
name=self._device_info.name,
|
name=self._device_info.name,
|
||||||
suggested_area=self._room_name,
|
sw_version=self._device_info.firmware,
|
||||||
sw_version=sw_version,
|
configuration_url=self._configuration_url,
|
||||||
configuration_url=f"http://{self._device_info.hub_address}/api/shades",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -77,42 +69,24 @@ class ShadeEntity(HDEntity):
|
|||||||
super().__init__(coordinator, device_info, room_name, shade.id)
|
super().__init__(coordinator, device_info, room_name, shade.id)
|
||||||
self._shade_name = shade_name
|
self._shade_name = shade_name
|
||||||
self._shade = shade
|
self._shade = shade
|
||||||
self._is_hard_wired = bool(
|
self._is_hard_wired = not shade.is_battery_powered()
|
||||||
shade.raw_data.get(ATTR_BATTERY_KIND) == BATTERY_KIND_HARDWIRED
|
self._configuration_url = shade.url
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions(self) -> PowerviewShadePositions:
|
def positions(self) -> ShadePosition:
|
||||||
"""Return the PowerviewShadeData."""
|
"""Return the PowerviewShadeData."""
|
||||||
return self.data.get_shade_positions(self._shade.id)
|
return self.data.get_shade_position(self._shade.id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self) -> DeviceInfo:
|
def device_info(self) -> DeviceInfo:
|
||||||
"""Return the device_info of the device."""
|
"""Return the device_info of the device."""
|
||||||
|
return DeviceInfo(
|
||||||
device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._shade.id)},
|
identifiers={(DOMAIN, self._shade.id)},
|
||||||
name=self._shade_name,
|
name=self._shade_name,
|
||||||
suggested_area=self._room_name,
|
suggested_area=self._room_name,
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=str(self._shade.raw_data[ATTR_TYPE]),
|
model=self._shade.type_name,
|
||||||
|
sw_version=self._shade.firmware,
|
||||||
via_device=(DOMAIN, self._device_info.serial_number),
|
via_device=(DOMAIN, self._device_info.serial_number),
|
||||||
configuration_url=(
|
configuration_url=self._configuration_url,
|
||||||
f"http://{self._device_info.hub_address}/api/shades/{self._shade.id}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for shade in self._shade.shade_types:
|
|
||||||
if str(shade.shade_type) == device_info[ATTR_MODEL]:
|
|
||||||
device_info[ATTR_MODEL] = shade.description
|
|
||||||
break
|
|
||||||
|
|
||||||
if FIRMWARE not in self._shade.raw_data:
|
|
||||||
return device_info
|
|
||||||
|
|
||||||
firmware = self._shade.raw_data[FIRMWARE]
|
|
||||||
sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}"
|
|
||||||
|
|
||||||
device_info[ATTR_SW_VERSION] = sw_version
|
|
||||||
|
|
||||||
return device_info
|
|
||||||
|
@ -18,6 +18,6 @@
|
|||||||
},
|
},
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiopvapi"],
|
"loggers": ["aiopvapi"],
|
||||||
"requirements": ["aiopvapi==2.0.4"],
|
"requirements": ["aiopvapi==3.0.2"],
|
||||||
"zeroconf": ["_powerview._tcp.local."]
|
"zeroconf": ["_powerview._tcp.local.", "_powerview-g3._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from aiopvapi.helpers.aiorequest import AioRequest
|
from aiopvapi.helpers.aiorequest import AioRequest
|
||||||
|
from aiopvapi.resources.room import Room
|
||||||
|
from aiopvapi.resources.scene import Scene
|
||||||
|
from aiopvapi.resources.shade import BaseShade
|
||||||
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
|
|
||||||
@ -14,9 +16,9 @@ class PowerviewEntryData:
|
|||||||
"""Define class for main domain information."""
|
"""Define class for main domain information."""
|
||||||
|
|
||||||
api: AioRequest
|
api: AioRequest
|
||||||
room_data: dict[str, Any]
|
room_data: dict[str, Room]
|
||||||
scene_data: dict[str, Any]
|
scene_data: dict[str, Scene]
|
||||||
shade_data: dict[str, Any]
|
shade_data: dict[str, BaseShade]
|
||||||
coordinator: PowerviewShadeUpdateCoordinator
|
coordinator: PowerviewShadeUpdateCoordinator
|
||||||
device_info: PowerviewDeviceInfo
|
device_info: PowerviewDeviceInfo
|
||||||
|
|
||||||
@ -28,6 +30,6 @@ class PowerviewDeviceInfo:
|
|||||||
name: str
|
name: str
|
||||||
mac_address: str
|
mac_address: str
|
||||||
serial_number: str
|
serial_number: str
|
||||||
firmware: dict[str, Any]
|
firmware: str | None
|
||||||
model: str
|
model: str
|
||||||
hub_address: str
|
hub_address: str
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"""Support for Powerview scenes from a Powerview hub."""
|
"""Support for Powerview scenes from a Powerview hub."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from aiopvapi.helpers.constants import ATTR_NAME
|
||||||
from aiopvapi.resources.scene import Scene as PvScene
|
from aiopvapi.resources.scene import Scene as PvScene
|
||||||
|
|
||||||
from homeassistant.components.scene import Scene
|
from homeassistant.components.scene import Scene
|
||||||
@ -10,11 +12,15 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN, ROOM_NAME_UNICODE, STATE_ATTRIBUTE_ROOM_NAME
|
from .const import DOMAIN, STATE_ATTRIBUTE_ROOM_NAME
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .entity import HDEntity
|
from .entity import HDEntity
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RESYNC_DELAY = 60
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
@ -24,9 +30,8 @@ async def async_setup_entry(
|
|||||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
pvscenes: list[PowerViewScene] = []
|
pvscenes: list[PowerViewScene] = []
|
||||||
for raw_scene in pv_entry.scene_data.values():
|
for scene in pv_entry.scene_data.values():
|
||||||
scene = PvScene(raw_scene, pv_entry.api)
|
room_name = getattr(pv_entry.room_data.get(scene.room_id), ATTR_NAME, "")
|
||||||
room_name = pv_entry.room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "")
|
|
||||||
pvscenes.append(
|
pvscenes.append(
|
||||||
PowerViewScene(pv_entry.coordinator, pv_entry.device_info, room_name, scene)
|
PowerViewScene(pv_entry.coordinator, pv_entry.device_info, room_name, scene)
|
||||||
)
|
)
|
||||||
@ -47,10 +52,11 @@ class PowerViewScene(HDEntity, Scene):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the scene."""
|
"""Initialize the scene."""
|
||||||
super().__init__(coordinator, device_info, room_name, scene.id)
|
super().__init__(coordinator, device_info, room_name, scene.id)
|
||||||
self._scene = scene
|
self._scene: PvScene = scene
|
||||||
self._attr_name = scene.name
|
self._attr_name = scene.name
|
||||||
self._attr_extra_state_attributes = {STATE_ATTRIBUTE_ROOM_NAME: room_name}
|
self._attr_extra_state_attributes = {STATE_ATTRIBUTE_ROOM_NAME: room_name}
|
||||||
|
|
||||||
async def async_activate(self, **kwargs: Any) -> None:
|
async def async_activate(self, **kwargs: Any) -> None:
|
||||||
"""Activate scene. Try to get entities into requested state."""
|
"""Activate scene. Try to get entities into requested state."""
|
||||||
await self._scene.activate()
|
shades = await self._scene.activate()
|
||||||
|
_LOGGER.debug("Scene activated for shade(s) %s", shades)
|
||||||
|
@ -3,9 +3,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aiopvapi.resources.shade import BaseShade, factory as PvShade
|
from aiopvapi.helpers.constants import ATTR_NAME, FUNCTION_SET_POWER
|
||||||
|
from aiopvapi.resources.shade import BaseShade
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -13,19 +15,13 @@ from homeassistant.const import EntityCategory
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
ATTR_BATTERY_KIND,
|
|
||||||
DOMAIN,
|
|
||||||
POWER_SUPPLY_TYPE_MAP,
|
|
||||||
POWER_SUPPLY_TYPE_REVERSE_MAP,
|
|
||||||
ROOM_ID_IN_SHADE,
|
|
||||||
ROOM_NAME_UNICODE,
|
|
||||||
SHADE_BATTERY_LEVEL,
|
|
||||||
)
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .entity import ShadeEntity
|
from .entity import ShadeEntity
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class PowerviewSelectDescriptionMixin:
|
class PowerviewSelectDescriptionMixin:
|
||||||
@ -33,6 +29,8 @@ class PowerviewSelectDescriptionMixin:
|
|||||||
|
|
||||||
current_fn: Callable[[BaseShade], Any]
|
current_fn: Callable[[BaseShade], Any]
|
||||||
select_fn: Callable[[BaseShade, str], Coroutine[Any, Any, bool]]
|
select_fn: Callable[[BaseShade, str], Coroutine[Any, Any, bool]]
|
||||||
|
create_entity_fn: Callable[[BaseShade], bool]
|
||||||
|
options_fn: Callable[[BaseShade], list[str]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -49,13 +47,10 @@ DROPDOWNS: Final = [
|
|||||||
key="powersource",
|
key="powersource",
|
||||||
translation_key="power_source",
|
translation_key="power_source",
|
||||||
icon="mdi:power-plug-outline",
|
icon="mdi:power-plug-outline",
|
||||||
current_fn=lambda shade: POWER_SUPPLY_TYPE_MAP.get(
|
current_fn=lambda shade: shade.get_power_source(),
|
||||||
shade.raw_data.get(ATTR_BATTERY_KIND), None
|
options_fn=lambda shade: shade.supported_power_sources(),
|
||||||
),
|
select_fn=lambda shade, option: shade.set_power_source(option),
|
||||||
options=list(POWER_SUPPLY_TYPE_MAP.values()),
|
create_entity_fn=lambda shade: shade.is_supported(FUNCTION_SET_POWER),
|
||||||
select_fn=lambda shade, option: shade.set_power_source(
|
|
||||||
POWER_SUPPLY_TYPE_REVERSE_MAP.get(option)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -67,26 +62,23 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
entities = []
|
entities: list[PowerViewSelect] = []
|
||||||
for raw_shade in pv_entry.shade_data.values():
|
for shade in pv_entry.shade_data.values():
|
||||||
shade: BaseShade = PvShade(raw_shade, pv_entry.api)
|
if not shade.has_battery_info():
|
||||||
if SHADE_BATTERY_LEVEL not in shade.raw_data:
|
|
||||||
continue
|
continue
|
||||||
name_before_refresh = shade.name
|
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")
|
||||||
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
|
|
||||||
room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
|
|
||||||
|
|
||||||
for description in DROPDOWNS:
|
for description in DROPDOWNS:
|
||||||
entities.append(
|
if description.create_entity_fn(shade):
|
||||||
PowerViewSelect(
|
entities.append(
|
||||||
pv_entry.coordinator,
|
PowerViewSelect(
|
||||||
pv_entry.device_info,
|
pv_entry.coordinator,
|
||||||
room_name,
|
pv_entry.device_info,
|
||||||
shade,
|
room_name,
|
||||||
name_before_refresh,
|
shade,
|
||||||
description,
|
shade.name,
|
||||||
|
description,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
@ -113,6 +105,11 @@ class PowerViewSelect(ShadeEntity, SelectEntity):
|
|||||||
"""Return the selected entity option to represent the entity state."""
|
"""Return the selected entity option to represent the entity state."""
|
||||||
return self.entity_description.current_fn(self._shade)
|
return self.entity_description.current_fn(self._shade)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self) -> list[str]:
|
||||||
|
"""Return a set of selectable options."""
|
||||||
|
return self.entity_description.options_fn(self._shade)
|
||||||
|
|
||||||
async def async_select_option(self, option: str) -> None:
|
async def async_select_option(self, option: str) -> None:
|
||||||
"""Change the selected option."""
|
"""Change the selected option."""
|
||||||
await self.entity_description.select_fn(self._shade, option)
|
await self.entity_description.select_fn(self._shade, option)
|
||||||
|
@ -4,7 +4,8 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aiopvapi.resources.shade import BaseShade, factory as PvShade
|
from aiopvapi.helpers.constants import ATTR_NAME
|
||||||
|
from aiopvapi.resources.shade import BaseShade
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -13,21 +14,11 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import DOMAIN
|
||||||
ATTR_BATTERY_KIND,
|
|
||||||
ATTR_SIGNAL_STRENGTH,
|
|
||||||
ATTR_SIGNAL_STRENGTH_MAX,
|
|
||||||
BATTERY_KIND_HARDWIRED,
|
|
||||||
DOMAIN,
|
|
||||||
ROOM_ID_IN_SHADE,
|
|
||||||
ROOM_NAME_UNICODE,
|
|
||||||
SHADE_BATTERY_LEVEL,
|
|
||||||
SHADE_BATTERY_LEVEL_MAX,
|
|
||||||
)
|
|
||||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||||
from .entity import ShadeEntity
|
from .entity import ShadeEntity
|
||||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||||
@ -38,8 +29,10 @@ class PowerviewSensorDescriptionMixin:
|
|||||||
"""Mixin to describe a Sensor entity."""
|
"""Mixin to describe a Sensor entity."""
|
||||||
|
|
||||||
update_fn: Callable[[BaseShade], Any]
|
update_fn: Callable[[BaseShade], Any]
|
||||||
|
device_class_fn: Callable[[BaseShade], SensorDeviceClass | None]
|
||||||
native_value_fn: Callable[[BaseShade], int]
|
native_value_fn: Callable[[BaseShade], int]
|
||||||
create_sensor_fn: Callable[[BaseShade], bool]
|
native_unit_fn: Callable[[BaseShade], str | None]
|
||||||
|
create_entity_fn: Callable[[BaseShade], bool]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -52,29 +45,33 @@ class PowerviewSensorDescription(
|
|||||||
state_class = SensorStateClass.MEASUREMENT
|
state_class = SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_device_class(shade: BaseShade) -> SensorDeviceClass | None:
|
||||||
|
"""Get the signal value based on version of API."""
|
||||||
|
return SensorDeviceClass.SIGNAL_STRENGTH if shade.api_version >= 3 else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_signal_native_unit(shade: BaseShade) -> str:
|
||||||
|
"""Get the unit of measurement for signal based on version of API."""
|
||||||
|
return SIGNAL_STRENGTH_DECIBELS if shade.api_version >= 3 else PERCENTAGE
|
||||||
|
|
||||||
|
|
||||||
SENSORS: Final = [
|
SENSORS: Final = [
|
||||||
PowerviewSensorDescription(
|
PowerviewSensorDescription(
|
||||||
key="charge",
|
key="charge",
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class_fn=lambda shade: SensorDeviceClass.BATTERY,
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_fn=lambda shade: PERCENTAGE,
|
||||||
native_value_fn=lambda shade: round(
|
native_value_fn=lambda shade: shade.get_battery_strength(),
|
||||||
shade.raw_data[SHADE_BATTERY_LEVEL] / SHADE_BATTERY_LEVEL_MAX * 100
|
create_entity_fn=lambda shade: shade.is_battery_powered(),
|
||||||
),
|
|
||||||
create_sensor_fn=lambda shade: bool(
|
|
||||||
shade.raw_data.get(ATTR_BATTERY_KIND) != BATTERY_KIND_HARDWIRED
|
|
||||||
and SHADE_BATTERY_LEVEL in shade.raw_data
|
|
||||||
),
|
|
||||||
update_fn=lambda shade: shade.refresh_battery(),
|
update_fn=lambda shade: shade.refresh_battery(),
|
||||||
),
|
),
|
||||||
PowerviewSensorDescription(
|
PowerviewSensorDescription(
|
||||||
key="signal",
|
key="signal",
|
||||||
translation_key="signal_strength",
|
translation_key="signal_strength",
|
||||||
icon="mdi:signal",
|
icon="mdi:signal",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
device_class_fn=get_signal_device_class,
|
||||||
native_value_fn=lambda shade: round(
|
native_unit_fn=get_signal_native_unit,
|
||||||
shade.raw_data[ATTR_SIGNAL_STRENGTH] / ATTR_SIGNAL_STRENGTH_MAX * 100
|
native_value_fn=lambda shade: shade.get_signal_strength(),
|
||||||
),
|
create_entity_fn=lambda shade: shade.has_signal_strength(),
|
||||||
create_sensor_fn=lambda shade: bool(ATTR_SIGNAL_STRENGTH in shade.raw_data),
|
|
||||||
update_fn=lambda shade: shade.refresh(),
|
update_fn=lambda shade: shade.refresh(),
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@ -89,21 +86,17 @@ async def async_setup_entry(
|
|||||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
|
||||||
entities: list[PowerViewSensor] = []
|
entities: list[PowerViewSensor] = []
|
||||||
for raw_shade in pv_entry.shade_data.values():
|
for shade in pv_entry.shade_data.values():
|
||||||
shade: BaseShade = PvShade(raw_shade, pv_entry.api)
|
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")
|
||||||
name_before_refresh = shade.name
|
|
||||||
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
|
|
||||||
room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
|
|
||||||
|
|
||||||
for description in SENSORS:
|
for description in SENSORS:
|
||||||
if description.create_sensor_fn(shade):
|
if description.create_entity_fn(shade):
|
||||||
entities.append(
|
entities.append(
|
||||||
PowerViewSensor(
|
PowerViewSensor(
|
||||||
pv_entry.coordinator,
|
pv_entry.coordinator,
|
||||||
pv_entry.device_info,
|
pv_entry.device_info,
|
||||||
room_name,
|
room_name,
|
||||||
shade,
|
shade,
|
||||||
name_before_refresh,
|
shade.name,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -125,17 +118,27 @@ class PowerViewSensor(ShadeEntity, SensorEntity):
|
|||||||
name: str,
|
name: str,
|
||||||
description: PowerviewSensorDescription,
|
description: PowerviewSensorDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the select entity."""
|
"""Initialize the sensor entity."""
|
||||||
super().__init__(coordinator, device_info, room_name, shade, name)
|
super().__init__(coordinator, device_info, room_name, shade, name)
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
|
self.entity_description: PowerviewSensorDescription = description
|
||||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||||
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int:
|
def native_value(self) -> int:
|
||||||
"""Get the current value in percentage."""
|
"""Get the current value of the sensor."""
|
||||||
return self.entity_description.native_value_fn(self._shade)
|
return self.entity_description.native_value_fn(self._shade)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_unit_of_measurement(self) -> str | None:
|
||||||
|
"""Return native unit of measurement of sensor."""
|
||||||
|
return self.entity_description.native_unit_fn(self._shade)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self) -> SensorDeviceClass | None:
|
||||||
|
"""Return the class of this entity."""
|
||||||
|
return self.entity_description.device_class_fn(self._shade)
|
||||||
|
|
||||||
# pylint: disable-next=hass-missing-super-call
|
# pylint: disable-next=hass-missing-super-call
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
|
@ -1,59 +1,25 @@
|
|||||||
"""Shade data for the Hunter Douglas PowerView integration."""
|
"""Shade data for the Hunter Douglas PowerView integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiopvapi.helpers.constants import (
|
from aiopvapi.resources.model import PowerviewData
|
||||||
ATTR_ID,
|
from aiopvapi.resources.shade import BaseShade, ShadePosition
|
||||||
ATTR_POSITION1,
|
|
||||||
ATTR_POSITION2,
|
|
||||||
ATTR_POSITION_DATA,
|
|
||||||
ATTR_POSKIND1,
|
|
||||||
ATTR_POSKIND2,
|
|
||||||
ATTR_SHADE,
|
|
||||||
)
|
|
||||||
from aiopvapi.resources.shade import MIN_POSITION
|
|
||||||
|
|
||||||
from .const import POS_KIND_PRIMARY, POS_KIND_SECONDARY, POS_KIND_VANE
|
|
||||||
from .util import async_map_data_by_id
|
from .util import async_map_data_by_id
|
||||||
|
|
||||||
POSITIONS = ((ATTR_POSITION1, ATTR_POSKIND1), (ATTR_POSITION2, ATTR_POSKIND2))
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PowerviewShadeMove:
|
|
||||||
"""Request to move a powerview shade."""
|
|
||||||
|
|
||||||
# The positions to request on the hub
|
|
||||||
request: dict[str, int]
|
|
||||||
|
|
||||||
# The positions that will also change
|
|
||||||
# as a result of the request that the
|
|
||||||
# hub will not send back
|
|
||||||
new_positions: dict[int, int]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PowerviewShadePositions:
|
|
||||||
"""Positions for a powerview shade."""
|
|
||||||
|
|
||||||
primary: int = MIN_POSITION
|
|
||||||
secondary: int = MIN_POSITION
|
|
||||||
vane: int = MIN_POSITION
|
|
||||||
|
|
||||||
|
|
||||||
class PowerviewShadeData:
|
class PowerviewShadeData:
|
||||||
"""Coordinate shade data between multiple api calls."""
|
"""Coordinate shade data between multiple api calls."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Init the shade data."""
|
"""Init the shade data."""
|
||||||
self._group_data_by_id: dict[int, dict[str | int, Any]] = {}
|
self._group_data_by_id: dict[int, dict[str | int, Any]] = {}
|
||||||
self.positions: dict[int, PowerviewShadePositions] = {}
|
self._shade_data_by_id: dict[int, BaseShade] = {}
|
||||||
|
self.positions: dict[int, ShadePosition] = {}
|
||||||
|
|
||||||
def get_raw_data(self, shade_id: int) -> dict[str | int, Any]:
|
def get_raw_data(self, shade_id: int) -> dict[str | int, Any]:
|
||||||
"""Get data for the shade."""
|
"""Get data for the shade."""
|
||||||
@ -63,17 +29,21 @@ class PowerviewShadeData:
|
|||||||
"""Get data for all shades."""
|
"""Get data for all shades."""
|
||||||
return self._group_data_by_id
|
return self._group_data_by_id
|
||||||
|
|
||||||
def get_shade_positions(self, shade_id: int) -> PowerviewShadePositions:
|
def get_shade(self, shade_id: int) -> BaseShade:
|
||||||
|
"""Get specific shade from the coordinator."""
|
||||||
|
return self._shade_data_by_id[shade_id]
|
||||||
|
|
||||||
|
def get_shade_position(self, shade_id: int) -> ShadePosition:
|
||||||
"""Get positions for a shade."""
|
"""Get positions for a shade."""
|
||||||
if shade_id not in self.positions:
|
if shade_id not in self.positions:
|
||||||
self.positions[shade_id] = PowerviewShadePositions()
|
self.positions[shade_id] = ShadePosition()
|
||||||
return self.positions[shade_id]
|
return self.positions[shade_id]
|
||||||
|
|
||||||
def update_from_group_data(self, shade_id: int) -> None:
|
def update_from_group_data(self, shade_id: int) -> None:
|
||||||
"""Process an update from the group data."""
|
"""Process an update from the group data."""
|
||||||
self.update_shade_positions(self._group_data_by_id[shade_id])
|
self.update_shade_positions(self._shade_data_by_id[shade_id])
|
||||||
|
|
||||||
def store_group_data(self, shade_data: Iterable[dict[str | int, Any]]) -> None:
|
def store_group_data(self, shade_data: PowerviewData) -> None:
|
||||||
"""Store data from the all shades endpoint.
|
"""Store data from the all shades endpoint.
|
||||||
|
|
||||||
This does not update the shades or positions
|
This does not update the shades or positions
|
||||||
@ -81,37 +51,34 @@ class PowerviewShadeData:
|
|||||||
with a shade_id will update a specific shade
|
with a shade_id will update a specific shade
|
||||||
from the group data.
|
from the group data.
|
||||||
"""
|
"""
|
||||||
self._group_data_by_id = async_map_data_by_id(shade_data)
|
self._shade_data_by_id = shade_data.processed
|
||||||
|
self._group_data_by_id = async_map_data_by_id(shade_data.raw)
|
||||||
|
|
||||||
def update_shade_position(self, shade_id: int, position: int, kind: int) -> None:
|
def update_shade_position(self, shade_id: int, shade_data: ShadePosition) -> None:
|
||||||
"""Update a single shade position."""
|
"""Update a single shades position."""
|
||||||
positions = self.get_shade_positions(shade_id)
|
if shade_id not in self.positions:
|
||||||
if kind == POS_KIND_PRIMARY:
|
self.positions[shade_id] = ShadePosition()
|
||||||
positions.primary = position
|
|
||||||
elif kind == POS_KIND_SECONDARY:
|
|
||||||
positions.secondary = position
|
|
||||||
elif kind == POS_KIND_VANE:
|
|
||||||
positions.vane = position
|
|
||||||
|
|
||||||
def update_from_position_data(
|
# ShadePosition will return None if the value is not set
|
||||||
self, shade_id: int, position_data: dict[str, Any]
|
if shade_data.primary is not None:
|
||||||
) -> None:
|
self.positions[shade_id].primary = shade_data.primary
|
||||||
"""Update the shade positions from the position data."""
|
if shade_data.secondary is not None:
|
||||||
for position_key, kind_key in POSITIONS:
|
self.positions[shade_id].secondary = shade_data.secondary
|
||||||
if position_key in position_data:
|
if shade_data.tilt is not None:
|
||||||
self.update_shade_position(
|
self.positions[shade_id].tilt = shade_data.tilt
|
||||||
shade_id, position_data[position_key], position_data[kind_key]
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_shade_positions(self, data: dict[int | str, Any]) -> None:
|
def update_shade_velocity(self, shade_id: int, shade_data: ShadePosition) -> None:
|
||||||
|
"""Update a single shades velocity."""
|
||||||
|
if shade_id not in self.positions:
|
||||||
|
self.positions[shade_id] = ShadePosition()
|
||||||
|
|
||||||
|
# the hub will always return a velocity of 0 on initial connect,
|
||||||
|
# separate definition to store consistent value in HA
|
||||||
|
# this value is purely driven from HA
|
||||||
|
if shade_data.velocity is not None:
|
||||||
|
self.positions[shade_id].velocity = shade_data.velocity
|
||||||
|
|
||||||
|
def update_shade_positions(self, data: BaseShade) -> None:
|
||||||
"""Update a shades from data dict."""
|
"""Update a shades from data dict."""
|
||||||
_LOGGER.debug("Raw data update: %s", data)
|
_LOGGER.debug("Raw data update: %s", data.raw_data)
|
||||||
shade_id = data[ATTR_ID]
|
self.update_shade_position(data.id, data.current_position)
|
||||||
position_data = data[ATTR_POSITION_DATA]
|
|
||||||
self.update_from_position_data(shade_id, position_data)
|
|
||||||
|
|
||||||
def update_from_response(self, response: dict[str, Any]) -> None:
|
|
||||||
"""Update from the response to a command."""
|
|
||||||
if response and ATTR_SHADE in response:
|
|
||||||
shade_data: dict[int | str, Any] = response[ATTR_SHADE]
|
|
||||||
self.update_shade_positions(shade_data)
|
|
||||||
|
@ -4,7 +4,11 @@
|
|||||||
"user": {
|
"user": {
|
||||||
"title": "Connect to the PowerView Hub",
|
"title": "Connect to the PowerView Hub",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::ip%]"
|
"host": "[%key:common::config_flow::data::ip%]",
|
||||||
|
"api_version": "Hub Generation"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"api_version": "API version is detectable, but you can override and force a specific version"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"link": {
|
"link": {
|
||||||
@ -15,6 +19,7 @@
|
|||||||
"flow_title": "{name} ({host})",
|
"flow_title": "{name} ({host})",
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unsupported_device": "Only the primary powerview hub can be added",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
|
@ -620,6 +620,11 @@ ZEROCONF = {
|
|||||||
"domain": "plugwise",
|
"domain": "plugwise",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"_powerview-g3._tcp.local.": [
|
||||||
|
{
|
||||||
|
"domain": "hunterdouglas_powerview",
|
||||||
|
},
|
||||||
|
],
|
||||||
"_powerview._tcp.local.": [
|
"_powerview._tcp.local.": [
|
||||||
{
|
{
|
||||||
"domain": "hunterdouglas_powerview",
|
"domain": "hunterdouglas_powerview",
|
||||||
|
@ -333,7 +333,7 @@ aiopulse==0.4.4
|
|||||||
aiopurpleair==2022.12.1
|
aiopurpleair==2022.12.1
|
||||||
|
|
||||||
# homeassistant.components.hunterdouglas_powerview
|
# homeassistant.components.hunterdouglas_powerview
|
||||||
aiopvapi==2.0.4
|
aiopvapi==3.0.2
|
||||||
|
|
||||||
# homeassistant.components.pvpc_hourly_pricing
|
# homeassistant.components.pvpc_hourly_pricing
|
||||||
aiopvpc==4.2.2
|
aiopvpc==4.2.2
|
||||||
|
@ -306,7 +306,7 @@ aiopulse==0.4.4
|
|||||||
aiopurpleair==2022.12.1
|
aiopurpleair==2022.12.1
|
||||||
|
|
||||||
# homeassistant.components.hunterdouglas_powerview
|
# homeassistant.components.hunterdouglas_powerview
|
||||||
aiopvapi==2.0.4
|
aiopvapi==3.0.2
|
||||||
|
|
||||||
# homeassistant.components.pvpc_hourly_pricing
|
# homeassistant.components.pvpc_hourly_pricing
|
||||||
aiopvpc==4.2.2
|
aiopvpc==4.2.2
|
||||||
|
@ -1,3 +1 @@
|
|||||||
"""Tests for the Hunter Douglas PowerView integration."""
|
"""Tests for the Hunter Douglas PowerView integration."""
|
||||||
|
|
||||||
MOCK_MAC = "AA::BB::CC::DD::EE::FF"
|
|
||||||
|
@ -1,47 +1,137 @@
|
|||||||
"""Tests for the Hunter Douglas PowerView integration."""
|
"""Common fixtures for Hunter Douglas Powerview tests."""
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
|
from aiopvapi.resources.shade import ShadePosition
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.common import load_fixture
|
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
||||||
|
|
||||||
|
from tests.common import load_json_object_fixture, load_json_value_fixture
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def powerview_userdata():
|
|
||||||
"""Return the userdata fixture."""
|
|
||||||
return json.loads(load_fixture("hunterdouglas_powerview/userdata.json"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def powerview_fwversion():
|
|
||||||
"""Return the fwversion fixture."""
|
|
||||||
return json.loads(load_fixture("hunterdouglas_powerview/fwversion.json"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def powerview_scenes():
|
|
||||||
"""Return the scenes fixture."""
|
|
||||||
return json.loads(load_fixture("hunterdouglas_powerview/scenes.json"))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_powerview_v2_hub(powerview_userdata, powerview_fwversion, powerview_scenes):
|
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||||
"""Mock a Powerview v2 hub."""
|
"""Override async_setup_entry."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData.get_resources",
|
"homeassistant.components.hunterdouglas_powerview.async_setup_entry",
|
||||||
return_value=powerview_userdata,
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_hunterdouglas_hub(
|
||||||
|
device_json: str,
|
||||||
|
home_json: str,
|
||||||
|
firmware_json: str,
|
||||||
|
rooms_json: str,
|
||||||
|
scenes_json: str,
|
||||||
|
shades_json: str,
|
||||||
|
) -> Generator[MagicMock, None, None]:
|
||||||
|
"""Return a mocked Powerview Hub with all data populated."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data",
|
||||||
|
return_value=load_json_object_fixture(device_json, DOMAIN),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_home_data",
|
||||||
|
return_value=load_json_object_fixture(home_json, DOMAIN),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_raw_firmware",
|
||||||
|
return_value=load_json_object_fixture(firmware_json, DOMAIN),
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.Rooms.get_resources",
|
"homeassistant.components.hunterdouglas_powerview.Rooms.get_resources",
|
||||||
return_value={"roomData": []},
|
return_value=load_json_value_fixture(rooms_json, DOMAIN),
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.Scenes.get_resources",
|
"homeassistant.components.hunterdouglas_powerview.Scenes.get_resources",
|
||||||
return_value=powerview_scenes,
|
return_value=load_json_value_fixture(scenes_json, DOMAIN),
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.Shades.get_resources",
|
"homeassistant.components.hunterdouglas_powerview.Shades.get_resources",
|
||||||
return_value={"shadeData": []},
|
return_value=load_json_value_fixture(shades_json, DOMAIN),
|
||||||
), patch(
|
), patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.ApiEntryPoint",
|
"homeassistant.components.hunterdouglas_powerview.cover.BaseShade.refresh",
|
||||||
return_value=powerview_fwversion,
|
), patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.cover.BaseShade.current_position",
|
||||||
|
new_callable=PropertyMock,
|
||||||
|
return_value=ShadePosition(primary=0, secondary=0, tilt=0, velocity=0),
|
||||||
):
|
):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def device_json(api_version: int) -> str:
|
||||||
|
"""Return the request_raw_data fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen1/userdata.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/userdata.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/gateway/primary.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def home_json(api_version: int) -> str:
|
||||||
|
"""Return the request_home_data fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen1/userdata.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/userdata.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/home/home.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def firmware_json(api_version: int) -> str:
|
||||||
|
"""Return the request_raw_firmware fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen1/fwversion.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/fwversion.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/gateway/info.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def rooms_json(api_version: int) -> str:
|
||||||
|
"""Return the get_resources fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen2/rooms.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/rooms.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/home/rooms.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def scenes_json(api_version: int) -> str:
|
||||||
|
"""Return the get_resources fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen2/scenes.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/scenes.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/home/scenes.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def shades_json(api_version: int) -> str:
|
||||||
|
"""Return the get_resources fixture for a specific device."""
|
||||||
|
if api_version == 1:
|
||||||
|
return "gen2/shades.json"
|
||||||
|
if api_version == 2:
|
||||||
|
return "gen2/shades.json"
|
||||||
|
if api_version == 3:
|
||||||
|
return "gen3/home/shades.json"
|
||||||
|
# Add more conditions for different api_versions if needed
|
||||||
|
raise ValueError(f"Unsupported api_version: {api_version}")
|
||||||
|
98
tests/components/hunterdouglas_powerview/const.py
Normal file
98
tests/components/hunterdouglas_powerview/const.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
"""Constants for Hunter Douglas Powerview tests."""
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
from homeassistant.components import dhcp, zeroconf
|
||||||
|
|
||||||
|
MOCK_MAC = "AA::BB::CC::DD::EE::FF"
|
||||||
|
|
||||||
|
HOMEKIT_DISCOVERY_GEN2 = zeroconf.ZeroconfServiceInfo(
|
||||||
|
ip_address="1.2.3.4",
|
||||||
|
ip_addresses=[IPv4Address("1.2.3.4")],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="Powerview Generation 2._hap._tcp.local.",
|
||||||
|
port=None,
|
||||||
|
properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC},
|
||||||
|
type="mock_type",
|
||||||
|
)
|
||||||
|
|
||||||
|
HOMEKIT_DISCOVERY_GEN3 = zeroconf.ZeroconfServiceInfo(
|
||||||
|
ip_address="1.2.3.4",
|
||||||
|
ip_addresses=[IPv4Address("1.2.3.4")],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="Powerview Generation 3._hap._tcp.local.",
|
||||||
|
port=None,
|
||||||
|
properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC},
|
||||||
|
type="mock_type",
|
||||||
|
)
|
||||||
|
|
||||||
|
ZEROCONF_DISCOVERY_GEN2 = zeroconf.ZeroconfServiceInfo(
|
||||||
|
ip_address="1.2.3.4",
|
||||||
|
ip_addresses=[IPv4Address("1.2.3.4")],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="Powerview Generation 2._powerview._tcp.local.",
|
||||||
|
port=None,
|
||||||
|
properties={},
|
||||||
|
type="mock_type",
|
||||||
|
)
|
||||||
|
|
||||||
|
ZEROCONF_DISCOVERY_GEN3 = zeroconf.ZeroconfServiceInfo(
|
||||||
|
ip_address="1.2.3.4",
|
||||||
|
ip_addresses=[IPv4Address("1.2.3.4")],
|
||||||
|
hostname="mock_hostname",
|
||||||
|
name="Powerview Generation 3._powerview-g3._tcp.local.",
|
||||||
|
port=None,
|
||||||
|
properties={},
|
||||||
|
type="mock_type",
|
||||||
|
)
|
||||||
|
|
||||||
|
DHCP_DISCOVERY_GEN2 = dhcp.DhcpServiceInfo(
|
||||||
|
hostname="Powerview Generation 2",
|
||||||
|
ip="1.2.3.4",
|
||||||
|
macaddress="aabbccddeeff",
|
||||||
|
)
|
||||||
|
|
||||||
|
DHCP_DISCOVERY_GEN3 = dhcp.DhcpServiceInfo(
|
||||||
|
hostname="Powerview Generation 3",
|
||||||
|
ip="1.2.3.4",
|
||||||
|
macaddress="aabbccddeeff",
|
||||||
|
)
|
||||||
|
|
||||||
|
HOMEKIT_DATA = [
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_HOMEKIT,
|
||||||
|
HOMEKIT_DISCOVERY_GEN2,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_HOMEKIT,
|
||||||
|
HOMEKIT_DISCOVERY_GEN3,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
DHCP_DATA = [
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_DHCP,
|
||||||
|
DHCP_DISCOVERY_GEN2,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_DHCP,
|
||||||
|
DHCP_DISCOVERY_GEN3,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
ZEROCONF_DATA = [
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_ZEROCONF,
|
||||||
|
ZEROCONF_DISCOVERY_GEN2,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
config_entries.SOURCE_ZEROCONF,
|
||||||
|
ZEROCONF_DISCOVERY_GEN3,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
DISCOVERY_DATA = HOMEKIT_DATA + DHCP_DATA + ZEROCONF_DATA
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"firmware": {
|
"firmware": {
|
||||||
"mainProcessor": {
|
"mainProcessor": {
|
||||||
"name": "PowerView Hub",
|
"name": "Powerview Generation 1",
|
||||||
"revision": 1,
|
"revision": 1,
|
||||||
"subRevision": 1,
|
"subRevision": 1,
|
||||||
"build": 857
|
"build": 857
|
@ -5,7 +5,7 @@
|
|||||||
"sceneControllerCount": 0,
|
"sceneControllerCount": 0,
|
||||||
"accessPointCount": 0,
|
"accessPointCount": 0,
|
||||||
"shadeCount": 5,
|
"shadeCount": 5,
|
||||||
"ip": "192.168.20.9",
|
"ip": "192.168.0.20",
|
||||||
"groupCount": 9,
|
"groupCount": 9,
|
||||||
"scheduledEventCount": 0,
|
"scheduledEventCount": 0,
|
||||||
"editingEnabled": true,
|
"editingEnabled": true,
|
||||||
@ -14,21 +14,21 @@
|
|||||||
"sceneCount": 18,
|
"sceneCount": 18,
|
||||||
"sceneControllerMemberCount": 0,
|
"sceneControllerMemberCount": 0,
|
||||||
"mask": "255.255.255.0",
|
"mask": "255.255.255.0",
|
||||||
"hubName": "UG93ZXJWaWV3IEh1YiBHZW4gMQ==",
|
"hubName": "UG93ZXJ2aWV3IEdlbmVyYXRpb24gMQ==",
|
||||||
"rfID": "0x8B2A",
|
"rfID": "0x8B2A",
|
||||||
"remoteConnectEnabled": false,
|
"remoteConnectEnabled": false,
|
||||||
"multiSceneMemberCount": 0,
|
"multiSceneMemberCount": 0,
|
||||||
"rfStatus": 0,
|
"rfStatus": 0,
|
||||||
"serialNumber": "REMOVED",
|
"serialNumber": "A1B2C3D4E5G6H7",
|
||||||
"undefinedShadeCount": 0,
|
"undefinedShadeCount": 0,
|
||||||
"sceneMemberCount": 18,
|
"sceneMemberCount": 18,
|
||||||
"unassignedShadeCount": 0,
|
"unassignedShadeCount": 0,
|
||||||
"multiSceneCount": 0,
|
"multiSceneCount": 0,
|
||||||
"addressKind": "newPrimary",
|
"addressKind": "newPrimary",
|
||||||
"gateway": "192.168.20.1",
|
"gateway": "192.168.0.1",
|
||||||
"localTimeDataSet": true,
|
"localTimeDataSet": true,
|
||||||
"dns": "192.168.20.1",
|
"dns": "192.168.0.1",
|
||||||
"macAddress": "00:00:00:00:00:eb",
|
"macAddress": "AA:BB:CC:DD:EE:FF",
|
||||||
"rfIDInt": 35626
|
"rfIDInt": 35626
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"firmware": {
|
||||||
|
"mainProcessor": {
|
||||||
|
"name": "Powerview Generation 2",
|
||||||
|
"revision": 2,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 1056
|
||||||
|
},
|
||||||
|
"radio": {
|
||||||
|
"revision": 2,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 2610
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"repeaterIds": [36882, 46633, 56803],
|
||||||
|
"repeaterData": [
|
||||||
|
{
|
||||||
|
"id": 36882,
|
||||||
|
"blinkEnabled": true,
|
||||||
|
"roomId": 57323,
|
||||||
|
"groupId": 44797,
|
||||||
|
"name": "UmVwZWF0ZXIgSGFsbHdheQ==",
|
||||||
|
"name_unicode": "Repeater Hallway",
|
||||||
|
"color": {
|
||||||
|
"brightness": 100,
|
||||||
|
"red": 0,
|
||||||
|
"green": 0,
|
||||||
|
"blue": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46633,
|
||||||
|
"blinkEnabled": true,
|
||||||
|
"roomId": 57323,
|
||||||
|
"groupId": 44797,
|
||||||
|
"name": "UmVwZWF0ZXIgTWFzdGVyIEJlZHJvb20=",
|
||||||
|
"name_unicode": "Repeater Master Bedroom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56803,
|
||||||
|
"blinkEnabled": true,
|
||||||
|
"roomId": 57323,
|
||||||
|
"groupId": 44797,
|
||||||
|
"name": "UmVwZWF0ZXIgS2l0Y2hlbg==",
|
||||||
|
"name_unicode": "Repeater Kitchen",
|
||||||
|
"color": {
|
||||||
|
"green": 0,
|
||||||
|
"red": 255,
|
||||||
|
"blue": 0,
|
||||||
|
"brightness": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"roomIds": [
|
||||||
|
9910, 3304, 24002, 57323, 64218, 2030, 58286, 23884, 61856, 9996, 46225,
|
||||||
|
36919, 34870, 34274
|
||||||
|
],
|
||||||
|
"roomData": [
|
||||||
|
{
|
||||||
|
"order": 8,
|
||||||
|
"name": "S2l0Y2hlbg==",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 38,
|
||||||
|
"type": 0,
|
||||||
|
"id": 9910,
|
||||||
|
"name_unicode": "Kitchen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 6,
|
||||||
|
"name": "U3R1ZHk=",
|
||||||
|
"colorId": 10,
|
||||||
|
"iconId": 73,
|
||||||
|
"type": 0,
|
||||||
|
"id": 3304,
|
||||||
|
"name_unicode": "Study"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 4,
|
||||||
|
"name": "QmVkbW9vciAtIDQ=",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 91,
|
||||||
|
"type": 0,
|
||||||
|
"id": 24002,
|
||||||
|
"name_unicode": "Bedroom - 4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"name": "UmVwZWF0ZXJz",
|
||||||
|
"colorId": 15,
|
||||||
|
"iconId": 0,
|
||||||
|
"order": 9,
|
||||||
|
"id": 57323,
|
||||||
|
"name_unicode": "Repeaters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 12,
|
||||||
|
"name": "TGF1bmRyeQ==",
|
||||||
|
"colorId": 3,
|
||||||
|
"iconId": 50,
|
||||||
|
"type": 0,
|
||||||
|
"id": 64218,
|
||||||
|
"name_unicode": "Laundry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 1,
|
||||||
|
"name": "QmVkcm9vbSAtIE1hc3Rlcg==",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 12,
|
||||||
|
"type": 0,
|
||||||
|
"id": 2030,
|
||||||
|
"name_unicode": "Bedroom - Master"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 5,
|
||||||
|
"name": "TG91bmdlIFJvb20=",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 59,
|
||||||
|
"type": 0,
|
||||||
|
"id": 58286,
|
||||||
|
"name_unicode": "Lounge Room"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 2,
|
||||||
|
"name": "RGVmYXVsdCBSb29t",
|
||||||
|
"colorId": 15,
|
||||||
|
"iconId": 168,
|
||||||
|
"order": 0,
|
||||||
|
"id": 23884,
|
||||||
|
"name_unicode": "Default Room"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 2,
|
||||||
|
"name": "QmVkbW9vciAtIDI=",
|
||||||
|
"colorId": 11,
|
||||||
|
"iconId": 91,
|
||||||
|
"type": 0,
|
||||||
|
"id": 61856,
|
||||||
|
"name_unicode": "Bedroom - 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 10,
|
||||||
|
"name": "R2FyYWdl",
|
||||||
|
"colorId": 4,
|
||||||
|
"iconId": 0,
|
||||||
|
"type": 0,
|
||||||
|
"id": 9996,
|
||||||
|
"name_unicode": "Garage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 3,
|
||||||
|
"name": "QmVkbW9vciAtIDM=",
|
||||||
|
"colorId": 7,
|
||||||
|
"iconId": 91,
|
||||||
|
"type": 0,
|
||||||
|
"id": 46225,
|
||||||
|
"name_unicode": "Bedroom - 3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 13,
|
||||||
|
"name": "Q29tbW9u",
|
||||||
|
"colorId": 3,
|
||||||
|
"iconId": 50,
|
||||||
|
"type": 0,
|
||||||
|
"id": 36919,
|
||||||
|
"name_unicode": "Common"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 11,
|
||||||
|
"name": "UnVtcHVz",
|
||||||
|
"colorId": 3,
|
||||||
|
"iconId": 50,
|
||||||
|
"type": 0,
|
||||||
|
"id": 34870,
|
||||||
|
"name_unicode": "Rumpus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order": 7,
|
||||||
|
"name": "RmFtaWx5IFJvb20=",
|
||||||
|
"colorId": 8,
|
||||||
|
"iconId": 17,
|
||||||
|
"type": 0,
|
||||||
|
"id": 34274,
|
||||||
|
"name_unicode": "Family Room"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,551 @@
|
|||||||
|
{
|
||||||
|
"sceneMemberIds": [
|
||||||
|
64407, 13542, 43466, 881, 51360, 27003, 32733, 58908, 41516, 52855, 2125,
|
||||||
|
18781, 2697, 59661, 58301, 38062, 46934, 60041, 56318, 42923, 19317, 62337,
|
||||||
|
17806, 64046, 61754, 35188, 57585, 44607, 5621, 64848, 5692, 41162, 56783,
|
||||||
|
38058, 46346, 6358, 61891, 12137, 45552, 57019, 20718, 43661, 58875, 53326,
|
||||||
|
61328, 400, 45652, 52292, 19246, 30009
|
||||||
|
],
|
||||||
|
"sceneMemberData": [
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 64407,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 13542,
|
||||||
|
"sceneId": 14067,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 43466,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 40836,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 0,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 881,
|
||||||
|
"sceneId": 28856,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 40,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 9679
|
||||||
|
},
|
||||||
|
"id": 51360,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 17062,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 0,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 27003,
|
||||||
|
"sceneId": 48043,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 32733,
|
||||||
|
"sceneId": 36482,
|
||||||
|
"shadeId": 65396,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 58908,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 13542,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 0,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 41516,
|
||||||
|
"sceneId": 48043,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 52855,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 2125,
|
||||||
|
"sceneId": 36482,
|
||||||
|
"shadeId": 13542,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 18781,
|
||||||
|
"sceneId": 44767,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 30334
|
||||||
|
},
|
||||||
|
"id": 2697,
|
||||||
|
"sceneId": 959,
|
||||||
|
"shadeId": 40836,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 59661,
|
||||||
|
"sceneId": 48756,
|
||||||
|
"shadeId": 13542,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 58301,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 27
|
||||||
|
},
|
||||||
|
"id": 38062,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 40,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 46934,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 13028,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 60041,
|
||||||
|
"sceneId": 48043,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 56318,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 5359,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 16
|
||||||
|
},
|
||||||
|
"id": 42923,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 13542,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 19317,
|
||||||
|
"sceneId": 59103,
|
||||||
|
"shadeId": 40836,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 180,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 6300
|
||||||
|
},
|
||||||
|
"id": 62337,
|
||||||
|
"sceneId": 19525,
|
||||||
|
"shadeId": 13028,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 44624
|
||||||
|
},
|
||||||
|
"id": 17806,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 40836,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 64046,
|
||||||
|
"sceneId": 49070,
|
||||||
|
"shadeId": 5359,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 160
|
||||||
|
},
|
||||||
|
"id": 61754,
|
||||||
|
"sceneId": 25458,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 65535,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 35188,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 40458,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 57585,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 65396,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 44607,
|
||||||
|
"sceneId": 64679,
|
||||||
|
"shadeId": 40458,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 5621,
|
||||||
|
"sceneId": 59188,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 64848,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 5359,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 0,
|
||||||
|
"posKind1": 1,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 46745
|
||||||
|
},
|
||||||
|
"id": 5692,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 49782,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 41162,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 56783,
|
||||||
|
"sceneId": 6789,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 47,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 9570
|
||||||
|
},
|
||||||
|
"id": 38058,
|
||||||
|
"sceneId": 22498,
|
||||||
|
"shadeId": 17062,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 46346,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 40458,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 6358,
|
||||||
|
"sceneId": 3455,
|
||||||
|
"shadeId": 49782,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 23
|
||||||
|
},
|
||||||
|
"id": 61891,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 65396,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 67
|
||||||
|
},
|
||||||
|
"id": 12137,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 37688,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 45552,
|
||||||
|
"sceneId": 3455,
|
||||||
|
"shadeId": 13028,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"position1": 65535,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 57019,
|
||||||
|
"sceneId": 59188,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"id": 20718,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 49782,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 65535
|
||||||
|
},
|
||||||
|
"id": 43661,
|
||||||
|
"sceneId": 59188,
|
||||||
|
"shadeId": 49988,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 58875,
|
||||||
|
"sceneId": 48756,
|
||||||
|
"shadeId": 65396,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"id": 53326,
|
||||||
|
"sceneId": 51159,
|
||||||
|
"shadeId": 40458,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 480
|
||||||
|
},
|
||||||
|
"id": 61328,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"shadeId": 17062,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 480
|
||||||
|
},
|
||||||
|
"id": 400,
|
||||||
|
"sceneId": 49070,
|
||||||
|
"shadeId": 17062,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 8,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 6361
|
||||||
|
},
|
||||||
|
"id": 45652,
|
||||||
|
"sceneId": 19525,
|
||||||
|
"shadeId": 49782,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 16,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 47522
|
||||||
|
},
|
||||||
|
"id": 52292,
|
||||||
|
"sceneId": 61648,
|
||||||
|
"shadeId": 13028,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 64138
|
||||||
|
},
|
||||||
|
"id": 19246,
|
||||||
|
"sceneId": 59968,
|
||||||
|
"shadeId": 26355,
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"positions": {
|
||||||
|
"posKind2": 2,
|
||||||
|
"position1": 0,
|
||||||
|
"position2": 0,
|
||||||
|
"posKind1": 1
|
||||||
|
},
|
||||||
|
"id": 30009,
|
||||||
|
"sceneId": 22498,
|
||||||
|
"shadeId": 5359,
|
||||||
|
"type": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,206 @@
|
|||||||
|
{
|
||||||
|
"sceneIds": [
|
||||||
|
49070, 44767, 6789, 3455, 36482, 59968, 19525, 14067, 48756, 59103, 61648,
|
||||||
|
24626, 64679, 22498, 28856, 25458, 51159, 959
|
||||||
|
],
|
||||||
|
"sceneData": [
|
||||||
|
{
|
||||||
|
"roomId": 58286,
|
||||||
|
"name": "Q2xvc2UgTG91bmdlIFJvb20=",
|
||||||
|
"name_unicode": "Close Lounge Room",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 59,
|
||||||
|
"networkNumber": 8,
|
||||||
|
"id": 49070,
|
||||||
|
"order": 18,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 24002,
|
||||||
|
"name": "Q2xvc2UgQmVkIDQ=",
|
||||||
|
"name_unicode": "Close Bed 4",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 20,
|
||||||
|
"id": 44767,
|
||||||
|
"order": 8,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 61856,
|
||||||
|
"name": "Q2xvc2UgQmVkIDI=",
|
||||||
|
"name_unicode": "Close Bed 2",
|
||||||
|
"colorId": 11,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 12,
|
||||||
|
"id": 6789,
|
||||||
|
"order": 6,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 2030,
|
||||||
|
"name": "Q2xvc2UgTWFzdGVyIEJlZA==",
|
||||||
|
"name_unicode": "Close Master Bed",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 12,
|
||||||
|
"networkNumber": 19,
|
||||||
|
"id": 3455,
|
||||||
|
"order": 5,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 34274,
|
||||||
|
"name": "Q2xvc2UgRmFtaWx5",
|
||||||
|
"name_unicode": "Close Family",
|
||||||
|
"colorId": 8,
|
||||||
|
"iconId": 17,
|
||||||
|
"networkNumber": 28,
|
||||||
|
"id": 36482,
|
||||||
|
"order": 15,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 24002,
|
||||||
|
"name": "T3BlbiBCZWQgNA==",
|
||||||
|
"name_unicode": "Open Bed 4",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 16,
|
||||||
|
"id": 59968,
|
||||||
|
"order": 11,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 2030,
|
||||||
|
"name": "T3BlbiBNYXN0ZXIgQmVk",
|
||||||
|
"name_unicode": "Open Master Bed",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 12,
|
||||||
|
"networkNumber": 7,
|
||||||
|
"id": 19525,
|
||||||
|
"order": 2,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 46225,
|
||||||
|
"name": "T3BlbiBCZWQgMw==",
|
||||||
|
"name_unicode": "Open Bed 3",
|
||||||
|
"colorId": 7,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 17,
|
||||||
|
"id": 14067,
|
||||||
|
"order": 12,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 34274,
|
||||||
|
"name": "T3BlbiBGYW1pbHk=",
|
||||||
|
"name_unicode": "Open Family",
|
||||||
|
"colorId": 8,
|
||||||
|
"iconId": 17,
|
||||||
|
"networkNumber": 10,
|
||||||
|
"id": 48756,
|
||||||
|
"order": 10,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 3304,
|
||||||
|
"name": "Q2xvc2UgU3R1ZHk=",
|
||||||
|
"name_unicode": "Close Study",
|
||||||
|
"colorId": 10,
|
||||||
|
"iconId": 73,
|
||||||
|
"networkNumber": 14,
|
||||||
|
"id": 59103,
|
||||||
|
"order": 17,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 23884,
|
||||||
|
"name": "T3BlbiBBbGw=",
|
||||||
|
"name_unicode": "Open All",
|
||||||
|
"colorId": 11,
|
||||||
|
"iconId": 0,
|
||||||
|
"networkNumber": 29,
|
||||||
|
"id": 61648,
|
||||||
|
"order": 3,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 23884,
|
||||||
|
"name": "Q2xvc2UgQWxs",
|
||||||
|
"name_unicode": "Close All",
|
||||||
|
"colorId": 0,
|
||||||
|
"iconId": 0,
|
||||||
|
"networkNumber": 24,
|
||||||
|
"id": 24626,
|
||||||
|
"order": 4,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 9910,
|
||||||
|
"name": "T3BlbiBLaXRjaGVu",
|
||||||
|
"name_unicode": "Open Kitchen",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 38,
|
||||||
|
"networkNumber": 21,
|
||||||
|
"id": 64679,
|
||||||
|
"order": 9,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 58286,
|
||||||
|
"name": "T3BlbiBMb3VuZ2UgUm9vbQ==",
|
||||||
|
"name_unicode": "Open Lounge Room",
|
||||||
|
"colorId": 14,
|
||||||
|
"iconId": 59,
|
||||||
|
"networkNumber": 3,
|
||||||
|
"id": 22498,
|
||||||
|
"order": 19,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 61856,
|
||||||
|
"name": "T3BlbiBCZWQgMg==",
|
||||||
|
"name_unicode": "Open Bed 2",
|
||||||
|
"colorId": 11,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 30,
|
||||||
|
"id": 28856,
|
||||||
|
"order": 13,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 46225,
|
||||||
|
"name": "Q2xvc2UgQmVkIDM=",
|
||||||
|
"name_unicode": "Close Bed 3",
|
||||||
|
"colorId": 7,
|
||||||
|
"iconId": 91,
|
||||||
|
"networkNumber": 5,
|
||||||
|
"id": 25458,
|
||||||
|
"order": 7,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 9910,
|
||||||
|
"name": "Q2xvc2UgS2l0Y2hlbg==",
|
||||||
|
"name_unicode": "Close Kitchen",
|
||||||
|
"colorId": 9,
|
||||||
|
"iconId": 38,
|
||||||
|
"networkNumber": 18,
|
||||||
|
"id": 51159,
|
||||||
|
"order": 14,
|
||||||
|
"hkAssist": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"roomId": 3304,
|
||||||
|
"name": "T3BlbiBTdHVkeQ==",
|
||||||
|
"name_unicode": "Open Study",
|
||||||
|
"colorId": 10,
|
||||||
|
"iconId": 73,
|
||||||
|
"networkNumber": 23,
|
||||||
|
"id": 959,
|
||||||
|
"order": 16,
|
||||||
|
"hkAssist": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
{
|
||||||
|
"scheduledEventIds": [
|
||||||
|
44184, 55249, 61633, 3443, 20621, 37484, 38971, 6269, 38424, 26333, 46810,
|
||||||
|
20372
|
||||||
|
],
|
||||||
|
"scheduledEventData": [
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 59968,
|
||||||
|
"daySunday": false,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 7,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 44184
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 24626,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 2,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 55249
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 25458,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": false,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 2,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": -123,
|
||||||
|
"id": 61633
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 49070,
|
||||||
|
"daySunday": false,
|
||||||
|
"dayMonday": false,
|
||||||
|
"dayTuesday": false,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": false,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 1,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": -52,
|
||||||
|
"id": 3443
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 59103,
|
||||||
|
"daySunday": false,
|
||||||
|
"dayMonday": false,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": false,
|
||||||
|
"dayThursday": false,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 2,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": 203,
|
||||||
|
"id": 20621
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"sceneId": 64679,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 7,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 37484
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 14067,
|
||||||
|
"daySunday": false,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 7,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 38971
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 19525,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 10,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 6269
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 3455,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": false,
|
||||||
|
"dayTuesday": false,
|
||||||
|
"dayWednesday": false,
|
||||||
|
"dayThursday": false,
|
||||||
|
"dayFriday": false,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 20,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 38424
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 59968,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": false,
|
||||||
|
"dayTuesday": false,
|
||||||
|
"dayWednesday": false,
|
||||||
|
"dayThursday": false,
|
||||||
|
"dayFriday": false,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 1,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 26333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 59968,
|
||||||
|
"daySunday": false,
|
||||||
|
"dayMonday": false,
|
||||||
|
"dayTuesday": false,
|
||||||
|
"dayWednesday": false,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": false,
|
||||||
|
"daySaturday": false,
|
||||||
|
"eventType": 0,
|
||||||
|
"hour": 8,
|
||||||
|
"minute": 0,
|
||||||
|
"id": 46810
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"sceneId": 36482,
|
||||||
|
"daySunday": true,
|
||||||
|
"dayMonday": true,
|
||||||
|
"dayTuesday": true,
|
||||||
|
"dayWednesday": true,
|
||||||
|
"dayThursday": true,
|
||||||
|
"dayFriday": true,
|
||||||
|
"daySaturday": true,
|
||||||
|
"eventType": 1,
|
||||||
|
"hour": 0,
|
||||||
|
"minute": 419,
|
||||||
|
"id": 20372
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,422 @@
|
|||||||
|
{
|
||||||
|
"shadeIds": [
|
||||||
|
49782, 13542, 6539, 37688, 5359, 26355, 13028, 65396, 40458, 17062, 40836,
|
||||||
|
49988
|
||||||
|
],
|
||||||
|
"shadeData": [
|
||||||
|
{
|
||||||
|
"id": 13542,
|
||||||
|
"type": 6,
|
||||||
|
"batteryStatus": 0,
|
||||||
|
"batteryStrength": 0,
|
||||||
|
"roomId": 34274,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "RmFtaWx5IExlZnQ=",
|
||||||
|
"groupId": 11497,
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 0,
|
||||||
|
"batteryKind": "unassigned",
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Family Left",
|
||||||
|
"shade_api_class": "ShadeBottomUp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6539,
|
||||||
|
"type": 44,
|
||||||
|
"batteryStatus": 0,
|
||||||
|
"batteryStrength": 0,
|
||||||
|
"roomId": 34274,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "RmFtaWx5IENlbnRyZQ==",
|
||||||
|
"groupId": 11497,
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 0,
|
||||||
|
"batteryKind": "unassigned",
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 3,
|
||||||
|
"position3": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Family Centre",
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltOnClosed180"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65396,
|
||||||
|
"type": 18,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 168,
|
||||||
|
"roomId": 34274,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "RmFtaWx5IFJpZ2h0",
|
||||||
|
"groupId": 11497,
|
||||||
|
"signalStrength": 4,
|
||||||
|
"capabilities": 1,
|
||||||
|
"batteryKind": 1,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 3,
|
||||||
|
"position3": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Family Right",
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltOnClosed90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37688,
|
||||||
|
"type": 51,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 169,
|
||||||
|
"roomId": 61856,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "QmVkIDI=",
|
||||||
|
"groupId": 47813,
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 2,
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 3,
|
||||||
|
"position3": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Bed 2",
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltAnywhere"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49988,
|
||||||
|
"type": 71,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 169,
|
||||||
|
"roomId": 46225,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "QmVkIDM=",
|
||||||
|
"groupId": 30562,
|
||||||
|
"signalStrength": 4,
|
||||||
|
"capabilities": 3,
|
||||||
|
"batteryKind": 3,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Bed 3",
|
||||||
|
"shade_api_class": "ShadeVertical"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26355,
|
||||||
|
"type": 56,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 168,
|
||||||
|
"roomId": 24002,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 336
|
||||||
|
},
|
||||||
|
"name": "QmVkIDQ=",
|
||||||
|
"groupId": 15150,
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 4,
|
||||||
|
"batteryKind": "unassigned",
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 1,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Bed 4",
|
||||||
|
"shade_api_class": "ShadeVerticalTiltAnywhere"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17062,
|
||||||
|
"type": 66,
|
||||||
|
"capabilities": 5,
|
||||||
|
"batteryKind": 3,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 184,
|
||||||
|
"roomId": 58286,
|
||||||
|
"name": "TG91bmdlIFJvb20gTGVmdA==",
|
||||||
|
"groupId": 32458,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 366
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 3,
|
||||||
|
"position1": 0
|
||||||
|
},
|
||||||
|
"signalStrength": 4,
|
||||||
|
"name_unicode": "Lounge Room Left",
|
||||||
|
"shade_api_class": "ShadeTiltOnly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5359,
|
||||||
|
"type": 7,
|
||||||
|
"capabilities": 6,
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 182,
|
||||||
|
"roomId": 58286,
|
||||||
|
"name": "TG91bmdlIFJvb20gUmlnaHQ=",
|
||||||
|
"groupId": 32458,
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"signalStrength": 4,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 366
|
||||||
|
},
|
||||||
|
"name_unicode": "Lounge Room Right",
|
||||||
|
"shade_api_class": "ShadeTopDown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49782,
|
||||||
|
"type": 8,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 183,
|
||||||
|
"roomId": 2030,
|
||||||
|
"name": "TWFzdGVyIExlZnQ=",
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"groupId": 44256,
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 366
|
||||||
|
},
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 7,
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Master Left",
|
||||||
|
"shade_api_class": "ShadeTopDownBottomUp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13028,
|
||||||
|
"type": 79,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 183,
|
||||||
|
"roomId": 2030,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 366
|
||||||
|
},
|
||||||
|
"name": "TWFzdGVyIFJpZ2h0",
|
||||||
|
"groupId": 44256,
|
||||||
|
"signalStrength": 4,
|
||||||
|
"capabilities": 8,
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Master Right",
|
||||||
|
"shade_api_class": "ShadeDualOverlapped"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40458,
|
||||||
|
"type": 38,
|
||||||
|
"batteryStatus": 0,
|
||||||
|
"batteryStrength": 0,
|
||||||
|
"roomId": 9910,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 4,
|
||||||
|
"build": 1701
|
||||||
|
},
|
||||||
|
"name": "S2l0Y2hlbiBSb2xsZXI=",
|
||||||
|
"motor": {
|
||||||
|
"revision": 48,
|
||||||
|
"subRevision": 50,
|
||||||
|
"build": 11825
|
||||||
|
},
|
||||||
|
"groupId": 37320,
|
||||||
|
"signalStrength": 2,
|
||||||
|
"capabilities": 9,
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"name_unicode": "Kitchen Roller",
|
||||||
|
"shade_api_class": "ShadeDualOverlappedTilt90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40836,
|
||||||
|
"type": 999,
|
||||||
|
"capabilities": 10,
|
||||||
|
"batteryKind": 2,
|
||||||
|
"smartPowerSupply": {
|
||||||
|
"status": 0,
|
||||||
|
"id": 0,
|
||||||
|
"port": 0
|
||||||
|
},
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"batteryStrength": 183,
|
||||||
|
"roomId": 3304,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 1,
|
||||||
|
"subRevision": 8,
|
||||||
|
"build": 1944
|
||||||
|
},
|
||||||
|
"motor": {
|
||||||
|
"revision": 0,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 366
|
||||||
|
},
|
||||||
|
"name": "U3R1ZHkgRHVldHRl",
|
||||||
|
"groupId": 41844,
|
||||||
|
"positions": {
|
||||||
|
"posKind1": 1,
|
||||||
|
"position1": 0,
|
||||||
|
"posKind2": 2,
|
||||||
|
"position2": 0
|
||||||
|
},
|
||||||
|
"signalStrength": 4,
|
||||||
|
"name_unicode": "Study Duette",
|
||||||
|
"shade_api_class": "ShadeDualOverlappedTilt180"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"userData": {
|
||||||
|
"hubName": "UG93ZXJ2aWV3IEdlbmVyYXRpb24gMg==",
|
||||||
|
"localTimeDataSet": true,
|
||||||
|
"enableScheduledEvents": true,
|
||||||
|
"editingEnabled": true,
|
||||||
|
"setupCompleted": false,
|
||||||
|
"gateway": "192.168.0.1",
|
||||||
|
"dns": "192.168.0.1",
|
||||||
|
"staticIp": false,
|
||||||
|
"_id": "zX7RBfmotYK8LwcD",
|
||||||
|
"color": {
|
||||||
|
"red": 0,
|
||||||
|
"green": 0,
|
||||||
|
"blue": 255,
|
||||||
|
"brightness": 50
|
||||||
|
},
|
||||||
|
"autoBackup": true,
|
||||||
|
"ip": "192.168.0.21",
|
||||||
|
"macAddress": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"mask": "255.255.255.0",
|
||||||
|
"firmware": {
|
||||||
|
"mainProcessor": {
|
||||||
|
"name": "Powerview Generation 2",
|
||||||
|
"revision": 2,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 1056
|
||||||
|
},
|
||||||
|
"radio": {
|
||||||
|
"revision": 2,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 2610
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serialNumber": "A1B2C3D4E5G6H7",
|
||||||
|
"rfIDInt": 59742,
|
||||||
|
"rfID": "0xE95E",
|
||||||
|
"rfStatus": 0,
|
||||||
|
"ssid": "Hub-E5:G6:H7",
|
||||||
|
"wireless": false,
|
||||||
|
"times": {
|
||||||
|
"timezone": "Australia/Sydney",
|
||||||
|
"localSunriseTimeInMinutes": 372,
|
||||||
|
"localSunsetTimeInMinutes": 1071,
|
||||||
|
"currentOffset": 36000,
|
||||||
|
"longitude": 149.1244,
|
||||||
|
"latitude": -35.3081
|
||||||
|
},
|
||||||
|
"rcUp": true,
|
||||||
|
"brand": "HD",
|
||||||
|
"remoteConnectEnabled": true
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"fwVersion": "3.1.472",
|
||||||
|
"serialNumber": "A1B2C3D4E5G6H7"
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"rev": 1,
|
||||||
|
"hwVersion": "3.0.4",
|
||||||
|
"model": "Pro",
|
||||||
|
"brand": "HD",
|
||||||
|
"serialNumber": "A1B2C3D4E5G6H7",
|
||||||
|
"firmware": {
|
||||||
|
"mainProcessor": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 1,
|
||||||
|
"build": 472,
|
||||||
|
"name": "Powerview Generation 3"
|
||||||
|
},
|
||||||
|
"radios": [
|
||||||
|
{
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 39
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rangeMapper": {
|
||||||
|
"minRSSI": -90,
|
||||||
|
"levelingSeconds": 4
|
||||||
|
},
|
||||||
|
"homeAssoc": 60325,
|
||||||
|
"color": {
|
||||||
|
"red": 0,
|
||||||
|
"green": 0,
|
||||||
|
"blue": 255,
|
||||||
|
"brightness": 50
|
||||||
|
},
|
||||||
|
"cloudConfig": {
|
||||||
|
"homeId": "G3pH6kP87QX5rWFD2vL",
|
||||||
|
"gatewayId": 385,
|
||||||
|
"collection": "homes",
|
||||||
|
"cloudEnv": 1
|
||||||
|
},
|
||||||
|
"networkConfig": {
|
||||||
|
"staticIpEnabled": false,
|
||||||
|
"staticIp": {
|
||||||
|
"ip_address": "0.0.0.0",
|
||||||
|
"gateway_ip": "0.0.0.0",
|
||||||
|
"netmask": "0.0.0.0",
|
||||||
|
"dns": ["0.0.0.0"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networkStatus": {
|
||||||
|
"ipAddress": "192.168.0.20",
|
||||||
|
"activeInterface": "eth0",
|
||||||
|
"primaryMacAddress": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"dns": [],
|
||||||
|
"eth0": {
|
||||||
|
"name": "eth0",
|
||||||
|
"ip_address": "192.168.0.20",
|
||||||
|
"mac_address": "AA:BB:CC:DD:EE:FF",
|
||||||
|
"gateway_ip": "192.168.0.1",
|
||||||
|
"netmask": "255.255.255.0",
|
||||||
|
"type": "Wired"
|
||||||
|
},
|
||||||
|
"wlan0": {},
|
||||||
|
"internetState": "Connected",
|
||||||
|
"ssid": "Hub-E5:G6:H7"
|
||||||
|
},
|
||||||
|
"mgwConfig": {
|
||||||
|
"primary": true
|
||||||
|
},
|
||||||
|
"mgwStatus": {
|
||||||
|
"running": true
|
||||||
|
},
|
||||||
|
"ip": "192.168.0.20"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"rev": 1,
|
||||||
|
"hwVersion": "3.0.4",
|
||||||
|
"model": "Pro",
|
||||||
|
"brand": "HD",
|
||||||
|
"serialNumber": "Z9Y8X7W6V5U4T3",
|
||||||
|
"firmware": {
|
||||||
|
"mainProcessor": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 1,
|
||||||
|
"build": 472,
|
||||||
|
"name": "Powerview Generation 3"
|
||||||
|
},
|
||||||
|
"radios": [
|
||||||
|
{
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 39
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"rangeMapper": {
|
||||||
|
"minRSSI": -90,
|
||||||
|
"levelingSeconds": 4
|
||||||
|
},
|
||||||
|
"homeAssoc": 60325,
|
||||||
|
"color": {
|
||||||
|
"red": 0,
|
||||||
|
"green": 0,
|
||||||
|
"blue": 255,
|
||||||
|
"brightness": 50
|
||||||
|
},
|
||||||
|
"cloudConfig": {
|
||||||
|
"homeId": "G3pH6kP87QX5rWFD2vL",
|
||||||
|
"gatewayId": 431,
|
||||||
|
"collection": "homes",
|
||||||
|
"cloudEnv": 1
|
||||||
|
},
|
||||||
|
"networkConfig": {
|
||||||
|
"staticIpEnabled": false,
|
||||||
|
"staticIp": {
|
||||||
|
"ip_address": "0.0.0.0",
|
||||||
|
"gateway_ip": "0.0.0.0",
|
||||||
|
"netmask": "0.0.0.0",
|
||||||
|
"dns": ["0.0.0.0"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"networkStatus": {
|
||||||
|
"ipAddress": "192.168.0.21",
|
||||||
|
"activeInterface": "eth0",
|
||||||
|
"primaryMacAddress": "GG:HH:II:JJ:KK:LL",
|
||||||
|
"dns": [],
|
||||||
|
"eth0": {
|
||||||
|
"name": "eth0",
|
||||||
|
"ip_address": "192.168.0.21",
|
||||||
|
"mac_address": "GG:HH:II:JJ:KK:LL",
|
||||||
|
"gateway_ip": "192.168.0.1",
|
||||||
|
"netmask": "255.255.255.0",
|
||||||
|
"type": "Wired"
|
||||||
|
},
|
||||||
|
"wlan0": {},
|
||||||
|
"internetState": "Connected",
|
||||||
|
"ssid": "Hub-V5:U4:T3"
|
||||||
|
},
|
||||||
|
"mgwConfig": {
|
||||||
|
"primary": false
|
||||||
|
},
|
||||||
|
"mgwStatus": {
|
||||||
|
"running": true
|
||||||
|
},
|
||||||
|
"ip": "192.168.0.21"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 432,
|
||||||
|
"type": 14,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 195,
|
||||||
|
"sceneId": 330,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 433,
|
||||||
|
"type": 10,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 69,
|
||||||
|
"sceneId": 328,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 434,
|
||||||
|
"type": 14,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 228,
|
||||||
|
"sceneId": 314,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 439,
|
||||||
|
"type": 14,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 136,
|
||||||
|
"sceneId": 280,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 441,
|
||||||
|
"type": 10,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 21,
|
||||||
|
"sceneId": 278,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 443,
|
||||||
|
"type": 10,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 56,
|
||||||
|
"sceneId": 299,
|
||||||
|
"errorShd_Ids": [121]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 444,
|
||||||
|
"type": 10,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 244,
|
||||||
|
"sceneId": 344,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 445,
|
||||||
|
"type": 10,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 0,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 70,
|
||||||
|
"sceneId": 292,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 437,
|
||||||
|
"type": 14,
|
||||||
|
"enabled": true,
|
||||||
|
"days": 127,
|
||||||
|
"hour": 1,
|
||||||
|
"min": 0,
|
||||||
|
"bleId": 2,
|
||||||
|
"sceneId": 220,
|
||||||
|
"errorShd_Ids": []
|
||||||
|
}
|
||||||
|
]
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,83 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 217,
|
||||||
|
"name": "RmFtaWx5IFJvb20=",
|
||||||
|
"ptName": "Family Room",
|
||||||
|
"color": "4",
|
||||||
|
"icon": "12",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 236,
|
||||||
|
"name": "QmVkcm9vbSAy",
|
||||||
|
"ptName": "Bedroom 2",
|
||||||
|
"color": "5",
|
||||||
|
"icon": "7",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 252,
|
||||||
|
"name": "QmVkcm9vbSAz",
|
||||||
|
"ptName": "Bedroom 3",
|
||||||
|
"color": "11",
|
||||||
|
"icon": "160",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 277,
|
||||||
|
"name": "QmVkcm9vbSA0",
|
||||||
|
"ptName": "Bedroom 4",
|
||||||
|
"color": "12",
|
||||||
|
"icon": "116",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 291,
|
||||||
|
"name": "TG91bmdlIFJvb20=",
|
||||||
|
"ptName": "Lounge Room",
|
||||||
|
"color": "7",
|
||||||
|
"icon": "54",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 298,
|
||||||
|
"name": "TWFzdGVyIEJlZG1vcmQ=",
|
||||||
|
"ptName": "Master Bedroom",
|
||||||
|
"color": "1",
|
||||||
|
"icon": "100",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 311,
|
||||||
|
"name": "S2l0Y2hlbg==",
|
||||||
|
"ptName": "Kitchen",
|
||||||
|
"color": "6",
|
||||||
|
"icon": "12",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 327,
|
||||||
|
"name": "U3R1ZHk=",
|
||||||
|
"ptName": "Study",
|
||||||
|
"color": "9",
|
||||||
|
"icon": "123",
|
||||||
|
"type": 0,
|
||||||
|
"shadeGroups": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 216,
|
||||||
|
"name": "RGVmYXVsdCBSb29t",
|
||||||
|
"ptName": "Default Room",
|
||||||
|
"color": "15",
|
||||||
|
"icon": "162",
|
||||||
|
"type": 2,
|
||||||
|
"shadeGroups": []
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,182 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 218,
|
||||||
|
"name": "Q2xvc2UgTG91bmdlIFJvb20=",
|
||||||
|
"ptName": "Close Lounge Room",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "4",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [291],
|
||||||
|
"shadeIds": [110, 51]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 220,
|
||||||
|
"name": "Q2xvc2UgQmVkIDQ=",
|
||||||
|
"ptName": "Close Bed 4",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "4",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [277],
|
||||||
|
"shadeIds": [173]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 237,
|
||||||
|
"name": "Q2xvc2UgQmVkIDI=",
|
||||||
|
"ptName": "Close Bed 2",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "5",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [277],
|
||||||
|
"shadeIds": [236]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 239,
|
||||||
|
"name": "Q2xvc2UgTWFzdGVyIEJlZA==",
|
||||||
|
"ptName": "Close Master Bed",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "5",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [298],
|
||||||
|
"shadeIds": [118, 13]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 253,
|
||||||
|
"name": "Q2xvc2UgRmFtaWx5",
|
||||||
|
"ptName": "Close Family",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "11",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [217],
|
||||||
|
"shadeIds": [413, 22, 6539]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 255,
|
||||||
|
"name": "T3BlbiBCZWQgNA==",
|
||||||
|
"ptName": "Open Bed 4",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "11",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [277],
|
||||||
|
"shadeIds": [173]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 278,
|
||||||
|
"name": "T3BlbiBNYXN0ZXIgQmVk",
|
||||||
|
"ptName": "Open Master Bed",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "12",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [298],
|
||||||
|
"shadeIds": [118, 13]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 280,
|
||||||
|
"name": "T3BlbiBCZWQgMw==",
|
||||||
|
"ptName": "Open Bed 3",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "12",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [252],
|
||||||
|
"shadeIds": [46]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 292,
|
||||||
|
"name": "T3BlbiBGYW1pbHk=",
|
||||||
|
"ptName": "Open Family",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "7",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [217],
|
||||||
|
"shadeIds": [413, 22, 6539]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 294,
|
||||||
|
"name": "Q2xvc2UgU3R1ZHk=",
|
||||||
|
"ptName": "Close Study",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "7",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [327],
|
||||||
|
"shadeIds": [192]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 299,
|
||||||
|
"name": "T3BlbiBBbGw=",
|
||||||
|
"ptName": "Open All",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "1",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [217, 236, 252, 277, 291, 298, 311, 327, 216],
|
||||||
|
"shadeIds": [10, 13, 22, 46, 51, 110, 118, 173, 180, 192, 413, 6539]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 301,
|
||||||
|
"name": "Q2xvc2UgQWxs",
|
||||||
|
"ptName": "Close All",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "1",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [217, 236, 252, 277, 291, 298, 311, 327, 216],
|
||||||
|
"shadeIds": [10, 13, 22, 46, 51, 110, 118, 173, 180, 192, 413, 6539]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 312,
|
||||||
|
"name": "T3BlbiBLaXRjaGVu",
|
||||||
|
"ptName": "Open Kitchen",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "6",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [311],
|
||||||
|
"shadeIds": [180]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 314,
|
||||||
|
"name": "T3BlbiBMb3VuZ2UgUm9vbQ==",
|
||||||
|
"ptName": "Open Lounge Room",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "6",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [291],
|
||||||
|
"shadeIds": [110, 51]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 328,
|
||||||
|
"name": "T3BlbiBCZWQgMg==",
|
||||||
|
"ptName": "Open Bed 2",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "9",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [236],
|
||||||
|
"shadeIds": [236]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 330,
|
||||||
|
"name": "Q2xvc2UgQmVkIDM=",
|
||||||
|
"ptName": "Close Bed 3",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "9",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [252],
|
||||||
|
"shadeIds": [46]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 344,
|
||||||
|
"name": "Q2xvc2UgS2l0Y2hlbg==",
|
||||||
|
"ptName": "Close Kitchen",
|
||||||
|
"networkNumber": 45057,
|
||||||
|
"color": "10",
|
||||||
|
"icon": "183",
|
||||||
|
"roomIds": [311],
|
||||||
|
"shadeIds": [180]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 346,
|
||||||
|
"name": "T3BlbiBTdHVkeQ==",
|
||||||
|
"ptName": "Open Study",
|
||||||
|
"networkNumber": 45058,
|
||||||
|
"color": "10",
|
||||||
|
"icon": "185",
|
||||||
|
"roomIds": [327],
|
||||||
|
"shadeIds": [192]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,314 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 413,
|
||||||
|
"type": 6,
|
||||||
|
"name": "RmFtaWx5IExlZnQ=",
|
||||||
|
"ptName": "Family Left",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 0,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 217,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 0,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -71,
|
||||||
|
"bleName": "R23:FC42",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeBottomUp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"type": 44,
|
||||||
|
"name": "RmFtaWx5IENlbnRyZQ==",
|
||||||
|
"ptName": "Family Centre",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 0,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 217,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 0,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -74,
|
||||||
|
"bleName": "R23:BD25",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltOnClosed180"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6539,
|
||||||
|
"type": 18,
|
||||||
|
"name": "RmFtaWx5IFJpZ2h0",
|
||||||
|
"ptName": "Family Right",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 1,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 217,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 0,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -74,
|
||||||
|
"bleName": "R23:BD25",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltOnClosed90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": 51,
|
||||||
|
"name": "QmVkIDI=",
|
||||||
|
"ptName": "Bed 2",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 2,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 236,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -66,
|
||||||
|
"bleName": "R23:CE7A",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeBottomUpTiltAnywhere"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"type": 71,
|
||||||
|
"name": "QmVkIDM=",
|
||||||
|
"ptName": "Bed 3",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 3,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 252,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -72,
|
||||||
|
"bleName": "R23:77EC",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeVertical"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 173,
|
||||||
|
"type": 56,
|
||||||
|
"name": "QmVkIDQ=",
|
||||||
|
"ptName": "Bed 4",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 4,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 277,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -83,
|
||||||
|
"bleName": "R23:20CC",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeVerticalTiltAnywhere"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 110,
|
||||||
|
"type": 66,
|
||||||
|
"name": "TG91bmdlIFJvb20gTGVmdA==",
|
||||||
|
"ptName": "Lounge Room Left",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 5,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 291,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -71,
|
||||||
|
"bleName": "R23:9A5B",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeTiltOnly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"type": 7,
|
||||||
|
"name": "TG91bmdlIFJvb20gUmlnaHQ=",
|
||||||
|
"ptName": "Lounge Room Right",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 6,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 252,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -74,
|
||||||
|
"bleName": "R23:1043",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeTopDown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 118,
|
||||||
|
"type": 8,
|
||||||
|
"name": "TWFzdGVyIExlZnQ=",
|
||||||
|
"ptName": "Master Left",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 7,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 298,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -80,
|
||||||
|
"bleName": "R23:E2CD",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeTopDownBottomUp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"type": 79,
|
||||||
|
"name": "TWFzdGVyIFJpZ2h0",
|
||||||
|
"ptName": "Master Right",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 8,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 2,
|
||||||
|
"roomId": 236,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 309
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 1,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -60,
|
||||||
|
"bleName": "R23:AFDD",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeDualOverlapped"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 180,
|
||||||
|
"type": 38,
|
||||||
|
"name": "S2l0Y2hlbiBSb2xsZXI=",
|
||||||
|
"ptName": "Kitchen Roller",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 9,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 311,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 359
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 0,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -74,
|
||||||
|
"bleName": "R23:B07F",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeDualOverlappedTilt90"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 192,
|
||||||
|
"type": 999,
|
||||||
|
"name": "U3R1ZHkgRHVldHRl",
|
||||||
|
"ptName": "Study Duette",
|
||||||
|
"motion": null,
|
||||||
|
"capabilities": 10,
|
||||||
|
"powerType": 2,
|
||||||
|
"batteryStatus": 3,
|
||||||
|
"roomId": 327,
|
||||||
|
"firmware": {
|
||||||
|
"revision": 3,
|
||||||
|
"subRevision": 0,
|
||||||
|
"build": 359
|
||||||
|
},
|
||||||
|
"positions": {
|
||||||
|
"primary": 0,
|
||||||
|
"secondary": 0,
|
||||||
|
"tilt": 0,
|
||||||
|
"velocity": 0
|
||||||
|
},
|
||||||
|
"signalStrength": -66,
|
||||||
|
"bleName": "R23:E63C",
|
||||||
|
"shadeGroupIds": [],
|
||||||
|
"shade_api_class": "ShadeDualOverlappedTilt180"
|
||||||
|
}
|
||||||
|
]
|
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,187 +1,70 @@
|
|||||||
"""Test the Hunter Douglas Powerview config flow."""
|
"""Test the Hunter Douglas Powerview config flow."""
|
||||||
from ipaddress import ip_address
|
from unittest.mock import MagicMock, patch
|
||||||
import json
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import dhcp, zeroconf
|
from homeassistant.components import dhcp, zeroconf
|
||||||
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
||||||
|
from homeassistant.const import CONF_API_VERSION, CONF_HOST, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from . import MOCK_MAC
|
from .const import DHCP_DATA, DISCOVERY_DATA, HOMEKIT_DATA
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, load_fixture
|
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||||
|
|
||||||
ZEROCONF_HOST = "1.2.3.4"
|
|
||||||
HOMEKIT_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
|
|
||||||
ip_address=ip_address(ZEROCONF_HOST),
|
|
||||||
ip_addresses=[ip_address(ZEROCONF_HOST)],
|
|
||||||
hostname="mock_hostname",
|
|
||||||
name="Hunter Douglas Powerview Hub._hap._tcp.local.",
|
|
||||||
port=None,
|
|
||||||
properties={zeroconf.ATTR_PROPERTIES_ID: MOCK_MAC},
|
|
||||||
type="mock_type",
|
|
||||||
)
|
|
||||||
|
|
||||||
ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo(
|
|
||||||
ip_address=ip_address(ZEROCONF_HOST),
|
|
||||||
ip_addresses=[ip_address(ZEROCONF_HOST)],
|
|
||||||
hostname="mock_hostname",
|
|
||||||
name="Hunter Douglas Powerview Hub._powerview._tcp.local.",
|
|
||||||
port=None,
|
|
||||||
properties={},
|
|
||||||
type="mock_type",
|
|
||||||
)
|
|
||||||
|
|
||||||
DHCP_DISCOVERY_INFO = dhcp.DhcpServiceInfo(
|
|
||||||
hostname="Hunter Douglas Powerview Hub",
|
|
||||||
ip="1.2.3.4",
|
|
||||||
macaddress="aabbccddeeff",
|
|
||||||
)
|
|
||||||
|
|
||||||
DISCOVERY_DATA = [
|
|
||||||
(
|
|
||||||
config_entries.SOURCE_HOMEKIT,
|
|
||||||
HOMEKIT_DISCOVERY_INFO,
|
|
||||||
),
|
|
||||||
(
|
|
||||||
config_entries.SOURCE_DHCP,
|
|
||||||
DHCP_DISCOVERY_INFO,
|
|
||||||
),
|
|
||||||
(config_entries.SOURCE_ZEROCONF, ZEROCONF_DISCOVERY_INFO),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mock_powerview_userdata(userdata=None, get_resources=None):
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
mock_powerview_userdata = MagicMock()
|
@pytest.mark.parametrize("api_version", [1, 2, 3])
|
||||||
if not userdata:
|
async def test_user_form(
|
||||||
userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json"))
|
hass: HomeAssistant,
|
||||||
if get_resources:
|
mock_setup_entry: MagicMock,
|
||||||
mock_powerview_userdata.get_resources = AsyncMock(side_effect=get_resources)
|
api_version: int,
|
||||||
else:
|
) -> None:
|
||||||
mock_powerview_userdata.get_resources = AsyncMock(return_value=userdata)
|
|
||||||
return mock_powerview_userdata
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mock_powerview_legacy_userdata(userdata=None, get_resources=None):
|
|
||||||
mock_powerview_userdata_legacy = MagicMock()
|
|
||||||
if not userdata:
|
|
||||||
userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata_v1.json"))
|
|
||||||
if get_resources:
|
|
||||||
mock_powerview_userdata_legacy.get_resources = AsyncMock(
|
|
||||||
side_effect=get_resources
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
mock_powerview_userdata_legacy.get_resources = AsyncMock(return_value=userdata)
|
|
||||||
return mock_powerview_userdata_legacy
|
|
||||||
|
|
||||||
|
|
||||||
def _get_mock_powerview_fwversion(fwversion=None, get_resources=None):
|
|
||||||
mock_powerview_fwversion = MagicMock()
|
|
||||||
if not fwversion:
|
|
||||||
fwversion = json.loads(load_fixture("hunterdouglas_powerview/fwversion.json"))
|
|
||||||
if get_resources:
|
|
||||||
mock_powerview_fwversion.get_resources = AsyncMock(side_effect=get_resources)
|
|
||||||
else:
|
|
||||||
mock_powerview_fwversion.get_resources = AsyncMock(return_value=fwversion)
|
|
||||||
return mock_powerview_fwversion
|
|
||||||
|
|
||||||
|
|
||||||
async def test_user_form(hass: HomeAssistant) -> None:
|
|
||||||
"""Test we get the user form."""
|
"""Test we get the user form."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result["type"] == "form"
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
with patch(
|
result["flow_id"],
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
{CONF_HOST: "1.2.3.4"},
|
||||||
return_value=mock_powerview_userdata,
|
)
|
||||||
), patch(
|
await hass.async_block_till_done()
|
||||||
"homeassistant.components.hunterdouglas_powerview.async_setup_entry",
|
|
||||||
return_value=True,
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
) as mock_setup_entry:
|
assert result2["title"] == f"Powerview Generation {api_version}"
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
result["flow_id"],
|
assert result2["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
{"host": "1.2.3.4"},
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
|
||||||
assert result2["title"] == "AlexanderHD"
|
|
||||||
assert result2["data"] == {
|
|
||||||
"host": "1.2.3.4",
|
|
||||||
}
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
result3 = await hass.config_entries.flow.async_init(
|
result3 = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
assert result3["type"] == "form"
|
assert result3["type"] == FlowResultType.FORM
|
||||||
assert result3["errors"] == {}
|
assert result3["errors"] == {}
|
||||||
|
|
||||||
result4 = await hass.config_entries.flow.async_configure(
|
result4 = await hass.config_entries.flow.async_configure(
|
||||||
result3["flow_id"],
|
result3["flow_id"],
|
||||||
{"host": "1.2.3.4"},
|
{CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
assert result4["type"] == "abort"
|
assert result4["type"] == FlowResultType.ABORT
|
||||||
|
assert result4["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
async def test_user_form_legacy(hass: HomeAssistant) -> None:
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
"""Test we get the user form with a legacy device."""
|
@pytest.mark.parametrize(("source", "discovery_info", "api_version"), DISCOVERY_DATA)
|
||||||
|
|
||||||
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_legacy_userdata()
|
|
||||||
mock_powerview_fwversion = _get_mock_powerview_fwversion()
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
|
||||||
return_value=mock_powerview_userdata,
|
|
||||||
), patch(
|
|
||||||
"homeassistant.components.hunterdouglas_powerview.ApiEntryPoint",
|
|
||||||
return_value=mock_powerview_fwversion,
|
|
||||||
), 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"},
|
|
||||||
)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
|
||||||
assert result2["title"] == "PowerView Hub Gen 1"
|
|
||||||
assert result2["data"] == {
|
|
||||||
"host": "1.2.3.4",
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("source", "discovery_info"), DISCOVERY_DATA)
|
|
||||||
async def test_form_homekit_and_dhcp_cannot_connect(
|
async def test_form_homekit_and_dhcp_cannot_connect(
|
||||||
hass: HomeAssistant, source, discovery_info
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
source: str,
|
||||||
|
discovery_info: dhcp.DhcpServiceInfo,
|
||||||
|
api_version: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form with homekit and dhcp source."""
|
"""Test we get the form with homekit and dhcp source."""
|
||||||
|
|
||||||
@ -190,10 +73,9 @@ async def test_form_homekit_and_dhcp_cannot_connect(
|
|||||||
)
|
)
|
||||||
ignored_config_entry.add_to_hass(hass)
|
ignored_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata(get_resources=TimeoutError)
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
"homeassistant.components.hunterdouglas_powerview.Hub.query_firmware",
|
||||||
return_value=mock_powerview_userdata,
|
side_effect=TimeoutError,
|
||||||
):
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -201,13 +83,35 @@ async def test_form_homekit_and_dhcp_cannot_connect(
|
|||||||
data=discovery_info,
|
data=discovery_info,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == FlowResultType.ABORT
|
||||||
assert result["reason"] == "cannot_connect"
|
assert result["reason"] == "cannot_connect"
|
||||||
|
|
||||||
|
# test we can recover from the failed entry
|
||||||
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": source},
|
||||||
|
data=discovery_info,
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(("source", "discovery_info"), DISCOVERY_DATA)
|
result3 = await hass.config_entries.flow.async_configure(result2["flow_id"], {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == f"Powerview Generation {api_version}"
|
||||||
|
assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
|
assert result3["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize(("source", "discovery_info", "api_version"), DISCOVERY_DATA)
|
||||||
async def test_form_homekit_and_dhcp(
|
async def test_form_homekit_and_dhcp(
|
||||||
hass: HomeAssistant, source, discovery_info
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
source: str,
|
||||||
|
discovery_info: dhcp.DhcpServiceInfo | zeroconf.ZeroconfServiceInfo,
|
||||||
|
api_version: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we get the form with homekit and dhcp source."""
|
"""Test we get the form with homekit and dhcp source."""
|
||||||
|
|
||||||
@ -216,39 +120,28 @@ async def test_form_homekit_and_dhcp(
|
|||||||
)
|
)
|
||||||
ignored_config_entry.add_to_hass(hass)
|
ignored_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
result = await hass.config_entries.flow.async_init(
|
||||||
with patch(
|
DOMAIN,
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
context={"source": source},
|
||||||
return_value=mock_powerview_userdata,
|
data=discovery_info,
|
||||||
):
|
)
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": source},
|
|
||||||
data=discovery_info,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "form"
|
assert result["type"] == FlowResultType.FORM
|
||||||
assert result["step_id"] == "link"
|
assert result["step_id"] == "link"
|
||||||
assert result["errors"] is None
|
assert result["errors"] is None
|
||||||
assert result["description_placeholders"] == {
|
assert result["description_placeholders"] == {
|
||||||
"host": "1.2.3.4",
|
CONF_HOST: "1.2.3.4",
|
||||||
"name": "Hunter Douglas Powerview Hub",
|
CONF_NAME: f"Powerview Generation {api_version}",
|
||||||
|
CONF_API_VERSION: api_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch(
|
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
await hass.async_block_till_done()
|
||||||
return_value=mock_powerview_userdata,
|
|
||||||
), 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"], {})
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
assert result2["type"] == "create_entry"
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "Hunter Douglas Powerview Hub"
|
assert result2["title"] == f"Powerview Generation {api_version}"
|
||||||
assert result2["data"] == {"host": "1.2.3.4"}
|
assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
assert result2["result"].unique_id == "ABC123"
|
assert result2["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
@ -257,95 +150,199 @@ async def test_form_homekit_and_dhcp(
|
|||||||
context={"source": source},
|
context={"source": source},
|
||||||
data=discovery_info,
|
data=discovery_info,
|
||||||
)
|
)
|
||||||
assert result3["type"] == "abort"
|
assert result3["type"] == FlowResultType.ABORT
|
||||||
|
|
||||||
|
|
||||||
async def test_discovered_by_homekit_and_dhcp(hass: HomeAssistant) -> None:
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("homekit_source", "homekit_discovery", "api_version"), HOMEKIT_DATA
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("dhcp_source", "dhcp_discovery", "dhcp_api_version"), DHCP_DATA
|
||||||
|
)
|
||||||
|
async def test_discovered_by_homekit_and_dhcp(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
homekit_source: str,
|
||||||
|
homekit_discovery: zeroconf.ZeroconfServiceInfo,
|
||||||
|
api_version: int,
|
||||||
|
dhcp_source: str,
|
||||||
|
dhcp_discovery: dhcp.DhcpServiceInfo,
|
||||||
|
dhcp_api_version: int,
|
||||||
|
) -> None:
|
||||||
"""Test we get the form with homekit and abort for dhcp source when we get both."""
|
"""Test we get the form with homekit and abort for dhcp source when we get both."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
||||||
|
data=homekit_discovery,
|
||||||
|
)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata()
|
assert result["type"] == FlowResultType.FORM
|
||||||
with patch(
|
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
|
||||||
return_value=mock_powerview_userdata,
|
|
||||||
):
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
||||||
data=HOMEKIT_DISCOVERY_INFO,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result["type"] == "form"
|
|
||||||
assert result["step_id"] == "link"
|
assert result["step_id"] == "link"
|
||||||
|
|
||||||
with patch(
|
result2 = await hass.config_entries.flow.async_init(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
DOMAIN,
|
||||||
return_value=mock_powerview_userdata,
|
context={"source": config_entries.SOURCE_DHCP},
|
||||||
):
|
data=dhcp_discovery,
|
||||||
result2 = await hass.config_entries.flow.async_init(
|
)
|
||||||
DOMAIN,
|
|
||||||
context={"source": config_entries.SOURCE_DHCP},
|
|
||||||
data=DHCP_DISCOVERY_INFO,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] == "abort"
|
assert result2["type"] == FlowResultType.ABORT
|
||||||
assert result2["reason"] == "already_in_progress"
|
assert result2["reason"] == "already_in_progress"
|
||||||
|
|
||||||
|
|
||||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize("api_version", [1, 2, 3])
|
||||||
|
async def test_form_cannot_connect(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
api_version: int,
|
||||||
|
) -> None:
|
||||||
"""Test we handle cannot connect error."""
|
"""Test we handle cannot connect error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata(get_resources=TimeoutError)
|
# Simulate a timeout error
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
"homeassistant.components.hunterdouglas_powerview.Hub.query_firmware",
|
||||||
return_value=mock_powerview_userdata,
|
side_effect=TimeoutError,
|
||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{"host": "1.2.3.4"},
|
{CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] == "form"
|
assert result2["type"] == FlowResultType.FORM
|
||||||
assert result2["errors"] == {"base": "cannot_connect"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
# Now try again without the patch in place to make sure we can recover
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{CONF_HOST: "1.2.3.4"},
|
||||||
|
)
|
||||||
|
|
||||||
async def test_form_no_data(hass: HomeAssistant) -> None:
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == f"Powerview Generation {api_version}"
|
||||||
|
assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
|
assert result3["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize("api_version", [1, 2, 3])
|
||||||
|
async def test_form_no_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
api_version: int,
|
||||||
|
) -> None:
|
||||||
"""Test we handle no data being returned from the hub."""
|
"""Test we handle no data being returned from the hub."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}})
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data",
|
||||||
return_value=mock_powerview_userdata,
|
return_value={},
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_home_data",
|
||||||
|
return_value={},
|
||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{"host": "1.2.3.4"},
|
{CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] == "form"
|
assert result2["type"] == FlowResultType.FORM
|
||||||
assert result2["errors"] == {"base": "unknown"}
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
# Now try again without the patch in place to make sure we can recover
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{CONF_HOST: "1.2.3.4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == f"Powerview Generation {api_version}"
|
||||||
|
assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
|
assert result3["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_form_unknown_exception(hass: HomeAssistant) -> None:
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize("api_version", [1, 2, 3])
|
||||||
|
async def test_form_unknown_exception(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
api_version: int,
|
||||||
|
) -> None:
|
||||||
"""Test we handle unknown exception."""
|
"""Test we handle unknown exception."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_powerview_userdata = _get_mock_powerview_userdata(userdata={"userData": {}})
|
# Simulate a transient error
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.UserData",
|
"homeassistant.components.hunterdouglas_powerview.config_flow.Hub.query_firmware",
|
||||||
return_value=mock_powerview_userdata,
|
side_effect=SyntaxError,
|
||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{"host": "1.2.3.4"},
|
{CONF_HOST: "1.2.3.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] == "form"
|
assert result2["type"] == FlowResultType.FORM
|
||||||
assert result2["errors"] == {"base": "unknown"}
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
# Now try again without the patch in place to make sure we can recover
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{CONF_HOST: "1.2.3.4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result2["title"] == f"Powerview Generation {api_version}"
|
||||||
|
assert result2["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
|
assert result2["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_hunterdouglas_hub")
|
||||||
|
@pytest.mark.parametrize("api_version", [3]) # only gen 3 present secondary hubs
|
||||||
|
async def test_form_unsupported_device(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: MagicMock,
|
||||||
|
api_version: int,
|
||||||
|
) -> None:
|
||||||
|
"""Test unsupported device failure."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate a gen 3 secondary hub
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hunterdouglas_powerview.Hub.request_raw_data",
|
||||||
|
return_value=load_json_object_fixture("gen3/gateway/secondary.json", DOMAIN),
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_HOST: "1.2.3.4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == FlowResultType.FORM
|
||||||
|
assert result2["errors"] == {"base": "unsupported_device"}
|
||||||
|
|
||||||
|
# Now try again without the patch in place to make sure we can recover
|
||||||
|
result3 = await hass.config_entries.flow.async_configure(
|
||||||
|
result2["flow_id"],
|
||||||
|
{CONF_HOST: "1.2.3.4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result3["title"] == f"Powerview Generation {api_version}"
|
||||||
|
assert result3["data"] == {CONF_HOST: "1.2.3.4", CONF_API_VERSION: api_version}
|
||||||
|
assert result3["result"].unique_id == "A1B2C3D4E5G6H7"
|
||||||
|
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
@ -1,26 +1,111 @@
|
|||||||
"""Test the Hunter Douglas Powerview scene platform."""
|
"""Test the Hunter Douglas Powerview scene platform."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
from homeassistant.components.hunterdouglas_powerview.const import DOMAIN
|
||||||
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON
|
from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, SERVICE_TURN_ON
|
||||||
from homeassistant.const import STATE_UNKNOWN
|
from homeassistant.const import STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import MOCK_MAC
|
from .const import MOCK_MAC
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_scenes(hass: HomeAssistant, mock_powerview_v2_hub: None) -> None:
|
@pytest.mark.parametrize("api_version", [1, 2, 3])
|
||||||
|
async def test_scenes(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_hunterdouglas_hub: None,
|
||||||
|
api_version: int,
|
||||||
|
) -> None:
|
||||||
"""Test the scenes."""
|
"""Test the scenes."""
|
||||||
entry = MockConfigEntry(domain=DOMAIN, data={"host": "1.2.3.4"}, unique_id=MOCK_MAC)
|
entry = MockConfigEntry(domain=DOMAIN, data={"host": "1.2.3.4"}, unique_id=MOCK_MAC)
|
||||||
entry.add_to_hass(hass)
|
entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(hass.states.async_all()) == 2
|
assert hass.states.async_entity_ids_count(SCENE_DOMAIN) == 18
|
||||||
assert hass.states.get("scene.alexanderhd_one").state == STATE_UNKNOWN
|
assert (
|
||||||
assert hass.states.get("scene.alexanderhd_two").state == STATE_UNKNOWN
|
hass.states.get(
|
||||||
|
f"scene.powerview_generation_{api_version}_close_lounge_room"
|
||||||
|
).state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_bed_4").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_bed_2").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(
|
||||||
|
f"scene.powerview_generation_{api_version}_close_master_bed"
|
||||||
|
).state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_family").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_bed_4").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(
|
||||||
|
f"scene.powerview_generation_{api_version}_open_master_bed"
|
||||||
|
).state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_bed_3").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_family").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_study").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_all").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_all").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_kitchen").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(
|
||||||
|
f"scene.powerview_generation_{api_version}_open_lounge_room"
|
||||||
|
).state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_bed_2").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_bed_3").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_close_kitchen").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
hass.states.get(f"scene.powerview_generation_{api_version}_open_study").state
|
||||||
|
== STATE_UNKNOWN
|
||||||
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hunterdouglas_powerview.scene.PvScene.activate"
|
"homeassistant.components.hunterdouglas_powerview.scene.PvScene.activate"
|
||||||
@ -28,7 +113,7 @@ async def test_scenes(hass: HomeAssistant, mock_powerview_v2_hub: None) -> None:
|
|||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
SCENE_DOMAIN,
|
SCENE_DOMAIN,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{"entity_id": "scene.alexanderhd_one"},
|
{"entity_id": f"scene.powerview_generation_{api_version}_open_study"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user