Brunt package update with async, data update coordinator and config flow (#49714)
* implemented config_flow and dataupdatecoordinator * implemented config flow, dataupdatecoordinator and tests. * undid extra vscode task * fixed pylint errors * updates based on review * fix mypy in reauth * fast interval to 5 sec * fixed test patches and others from review * added released package * deleted wrong line from coveragerc * updates to config and tests * fixed test patch
This commit is contained in:
parent
05eb2f3e5c
commit
958c199d80
12 changed files with 586 additions and 79 deletions
|
@ -1,84 +1,134 @@
|
|||
"""Support for Brunt Blind Engine covers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from brunt import BruntAPI
|
||||
import voluptuous as vol
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from brunt import BruntClientAsync, Thing
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
PLATFORM_SCHEMA,
|
||||
DEVICE_CLASS_SHADE,
|
||||
SUPPORT_CLOSE,
|
||||
SUPPORT_OPEN,
|
||||
SUPPORT_SET_POSITION,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_REQUEST_POSITION,
|
||||
ATTRIBUTION,
|
||||
CLOSED_POSITION,
|
||||
DATA_BAPI,
|
||||
DATA_COOR,
|
||||
DOMAIN,
|
||||
FAST_INTERVAL,
|
||||
OPEN_POSITION,
|
||||
REGULAR_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
|
||||
ATTR_REQUEST_POSITION = "request_position"
|
||||
NOTIFICATION_ID = "brunt_notification"
|
||||
NOTIFICATION_TITLE = "Brunt Cover Setup"
|
||||
ATTRIBUTION = "Based on an unofficial Brunt SDK."
|
||||
|
||||
CLOSED_POSITION = 0
|
||||
OPEN_POSITION = 100
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the brunt platform."""
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
|
||||
bapi = BruntAPI(username=username, password=password)
|
||||
try:
|
||||
if not (things := bapi.getThings()["things"]):
|
||||
_LOGGER.error("No things present in account")
|
||||
else:
|
||||
add_entities(
|
||||
[
|
||||
BruntDevice(bapi, thing["NAME"], thing["thingUri"])
|
||||
for thing in things
|
||||
],
|
||||
True,
|
||||
)
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
hass.components.persistent_notification.create(
|
||||
"Error: {ex}<br />You will need to restart hass after fixing.",
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID,
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Component setup, run import config flow for each entry in config."""
|
||||
_LOGGER.warning(
|
||||
"Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class BruntDevice(CoverEntity):
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the brunt platform."""
|
||||
bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI]
|
||||
coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COOR]
|
||||
|
||||
async_add_entities(
|
||||
BruntDevice(coordinator, serial, thing, bapi, entry.entry_id)
|
||||
for serial, thing in coordinator.data.items()
|
||||
)
|
||||
|
||||
|
||||
class BruntDevice(CoordinatorEntity, CoverEntity):
|
||||
"""
|
||||
Representation of a Brunt cover device.
|
||||
|
||||
Contains the common logic for all Brunt devices.
|
||||
"""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_WINDOW
|
||||
_attr_supported_features = COVER_FEATURES
|
||||
|
||||
def __init__(self, bapi, name, thing_uri):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator,
|
||||
serial: str,
|
||||
thing: Thing,
|
||||
bapi: BruntClientAsync,
|
||||
entry_id: str,
|
||||
) -> None:
|
||||
"""Init the Brunt device."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = serial
|
||||
self._bapi = bapi
|
||||
self._attr_name = name
|
||||
self._thing_uri = thing_uri
|
||||
self._thing = thing
|
||||
self._entry_id = entry_id
|
||||
|
||||
self._state = {}
|
||||
self._remove_update_listener = None
|
||||
|
||||
self._attr_name = self._thing.NAME
|
||||
self._attr_device_class = DEVICE_CLASS_SHADE
|
||||
self._attr_supported_features = COVER_FEATURES
|
||||
self._attr_attribution = ATTRIBUTION
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
name=self._attr_name,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
manufacturer="Brunt",
|
||||
sw_version=self._thing.FW_VERSION,
|
||||
model=self._thing.MODEL,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(self._brunt_update_listener)
|
||||
)
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
"""
|
||||
Return current position of cover.
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
pos = self.coordinator.data[self.unique_id].currentPosition
|
||||
return int(pos) if pos is not None else None
|
||||
|
||||
@property
|
||||
def request_cover_position(self) -> int | None:
|
||||
|
@ -89,8 +139,8 @@ class BruntDevice(CoverEntity):
|
|||
to Brunt, at times there is a diff of 1 to current
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
pos = self._state.get("requestPosition")
|
||||
return int(pos) if pos else None
|
||||
pos = self.coordinator.data[self.unique_id].requestPosition
|
||||
return int(pos) if pos is not None else None
|
||||
|
||||
@property
|
||||
def move_state(self) -> int | None:
|
||||
|
@ -99,37 +149,64 @@ class BruntDevice(CoverEntity):
|
|||
|
||||
None is unknown, 0 when stopped, 1 when opening, 2 when closing
|
||||
"""
|
||||
mov = self._state.get("moveState")
|
||||
return int(mov) if mov else None
|
||||
mov = self.coordinator.data[self.unique_id].moveState
|
||||
return int(mov) if mov is not None else None
|
||||
|
||||
def update(self):
|
||||
"""Poll the current state of the device."""
|
||||
try:
|
||||
self._state = self._bapi.getState(thingUri=self._thing_uri).get("thing")
|
||||
self._attr_available = True
|
||||
except (TypeError, KeyError, NameError, ValueError) as ex:
|
||||
_LOGGER.error("%s", ex)
|
||||
self._attr_available = False
|
||||
self._attr_is_opening = self.move_state == 1
|
||||
self._attr_is_closing = self.move_state == 2
|
||||
pos = self._state.get("currentPosition")
|
||||
self._attr_current_cover_position = int(pos) if pos else None
|
||||
self._attr_is_closed = self.current_cover_position == CLOSED_POSITION
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
@property
|
||||
def is_opening(self) -> bool:
|
||||
"""Return if the cover is opening or not."""
|
||||
return self.move_state == 1
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool:
|
||||
"""Return if the cover is closing or not."""
|
||||
return self.move_state == 2
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> MutableMapping[str, Any]:
|
||||
"""Return the detailed device state attributes."""
|
||||
return {
|
||||
ATTR_REQUEST_POSITION: self.request_cover_position,
|
||||
}
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self.current_cover_position == CLOSED_POSITION
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Set the cover to the open position."""
|
||||
self._bapi.changeRequestPosition(OPEN_POSITION, thingUri=self._thing_uri)
|
||||
await self._async_update_cover(OPEN_POSITION)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Set the cover to the closed position."""
|
||||
self._bapi.changeRequestPosition(CLOSED_POSITION, thingUri=self._thing_uri)
|
||||
await self._async_update_cover(CLOSED_POSITION)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Set the cover to a specific position."""
|
||||
self._bapi.changeRequestPosition(
|
||||
kwargs[ATTR_POSITION], thingUri=self._thing_uri
|
||||
)
|
||||
await self._async_update_cover(int(kwargs[ATTR_POSITION]))
|
||||
|
||||
async def _async_update_cover(self, position: int) -> None:
|
||||
"""Set the cover to the new position and wait for the update to be reflected."""
|
||||
try:
|
||||
await self._bapi.async_change_request_position(
|
||||
position, thingUri=self._thing.thingUri
|
||||
)
|
||||
except ClientResponseError as exc:
|
||||
raise HomeAssistantError(
|
||||
f"Unable to reposition {self._thing.NAME}"
|
||||
) from exc
|
||||
self.coordinator.update_interval = FAST_INTERVAL
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def _brunt_update_listener(self) -> None:
|
||||
"""Update the update interval after each refresh."""
|
||||
if (
|
||||
self.request_cover_position
|
||||
== self._bapi.last_requested_positions[self._thing.thingUri]
|
||||
and self.move_state == 0
|
||||
):
|
||||
self.coordinator.update_interval = REGULAR_INTERVAL
|
||||
else:
|
||||
self.coordinator.update_interval = FAST_INTERVAL
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue