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:
Eduard van Valkenburg 2021-11-18 23:00:42 +01:00 committed by GitHub
parent 05eb2f3e5c
commit 958c199d80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 586 additions and 79 deletions

View file

@ -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