forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Acmeda integration (home-assistant#33384)
* First cut of Rollease Acmeda Pulse Hub integration. * Acmeda integration improvements: - Moved common code into a base entity - Battery level sensor added - Localisation now working * Added requirement for aiopulse now that it has been uploaded to PyPI. * Exclude acmeda integration from coverage check as it relies on a hub being present. * Fix Travis CI build issues. * Remove unused constants. * Remove unused group logic from cover.py * Removed commented code from base.py * Remove sensors (battery entities) on removal of hub. * Remove unused groups from sensor.py * Acmeda device and entity update made fully asynchronous using subscriptions to remove need for config polling. * Updated aiopulse version dependency. Removed non-functional battery charging indication. * Rationalised common code to update entities into helpers.py * Fix linting issue. * Correct additional CI pylint errors. * Index config_entries by entry_id. Move entity loading and unloading to __init__.py Add entry_id to dispatcher signal Removed now unused polling code hub Added config_flow unit tests * Tweak to integration config_entry title. * Bumped aiopulse module to 0.3.2. Reduced verbosity of aiopulse module. * Changed to using direct write of device state. Removed old style async_step_init config_flow step. * Remove superfluous battery_level and device_state_attributes from battery entity. * Removal of unused strings. Removal of unused create_config_flow helper. Removal of stale comment. * Remove use of shared container to track existing enities. Moved removal and deregistration of entities to base class through use of dispatch helper. * Fixed strings.json * Fix incorrect use of remove instead of pop on dict. * Add support for tilting covers, bump aiopulse version number. * Bump aiopulse version to v0.3.4. Fixed bug in cover supported_features. * Bumped aiopulse version to 0.4.0 Update acmeda .coveragerc exclusions * Removed already configured hub check from __init__.py async_setup_entry Removed passing in hass reference to base entity class Renamed entity async_reset to async_will_remove_from_hass Changed device_info and properties Migrated to CoveEntity from CoverDevice Added dispatched_connect cleanup on hub removal Removed unused entries from manifest Removed override of battery icon Renamed translations folder * Reversed unintended change to .coveragerc * Fixed config flow for multi-hub discovery. * Acmeda enhancements as requested by MartinHjelmare * Force import to connect to hub to retrieve id prior to creating entry * Remove YAML configuration support. * Tidied up config_flow and tests: - removed unnecessary steps - fixed typos * Removed storage of hub in config_flow.
- Loading branch information
Showing
19 changed files
with
736 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
"""The Rollease Acmeda Automate integration.""" | ||
import asyncio | ||
|
||
from homeassistant import config_entries, core | ||
|
||
from .const import DOMAIN | ||
from .hub import PulseHub | ||
|
||
CONF_HUBS = "hubs" | ||
|
||
PLATFORMS = ["cover", "sensor"] | ||
|
||
|
||
async def async_setup(hass: core.HomeAssistant, config: dict): | ||
"""Set up the Rollease Acmeda Automate component.""" | ||
return True | ||
|
||
|
||
async def async_setup_entry( | ||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry | ||
): | ||
"""Set up Rollease Acmeda Automate hub from a config entry.""" | ||
hub = PulseHub(hass, config_entry) | ||
|
||
if not await hub.async_setup(): | ||
return False | ||
|
||
hass.data.setdefault(DOMAIN, {}) | ||
hass.data[DOMAIN][config_entry.entry_id] = hub | ||
|
||
for component in PLATFORMS: | ||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(config_entry, component) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry( | ||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry | ||
): | ||
"""Unload a config entry.""" | ||
hub = hass.data[DOMAIN][config_entry.entry_id] | ||
|
||
unload_ok = all( | ||
await asyncio.gather( | ||
*[ | ||
hass.config_entries.async_forward_entry_unload(config_entry, component) | ||
for component in PLATFORMS | ||
] | ||
) | ||
) | ||
if not await hub.async_reset(): | ||
return False | ||
|
||
if unload_ok: | ||
hass.data[DOMAIN].pop(config_entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
"""Base class for Acmeda Roller Blinds.""" | ||
import aiopulse | ||
|
||
from homeassistant.core import callback | ||
from homeassistant.helpers import entity | ||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg | ||
from homeassistant.helpers.dispatcher import async_dispatcher_connect | ||
from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg | ||
|
||
from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER | ||
|
||
|
||
class AcmedaBase(entity.Entity): | ||
"""Base representation of an Acmeda roller.""" | ||
|
||
def __init__(self, roller: aiopulse.Roller): | ||
"""Initialize the roller.""" | ||
self.roller = roller | ||
|
||
async def async_remove_and_unregister(self): | ||
"""Unregister from entity and device registry and call entity remove function.""" | ||
LOGGER.error("Removing %s %s", self.__class__.__name__, self.unique_id) | ||
|
||
ent_registry = await get_ent_reg(self.hass) | ||
if self.entity_id in ent_registry.entities: | ||
ent_registry.async_remove(self.entity_id) | ||
|
||
dev_registry = await get_dev_reg(self.hass) | ||
device = dev_registry.async_get_device( | ||
identifiers={(DOMAIN, self.unique_id)}, connections=set() | ||
) | ||
if device is not None: | ||
dev_registry.async_update_device( | ||
device.id, remove_config_entry_id=self.registry_entry.config_entry_id | ||
) | ||
|
||
await self.async_remove() | ||
|
||
async def async_added_to_hass(self): | ||
"""Entity has been added to hass.""" | ||
self.roller.callback_subscribe(self.notify_update) | ||
|
||
self.async_on_remove( | ||
async_dispatcher_connect( | ||
self.hass, | ||
ACMEDA_ENTITY_REMOVE.format(self.roller.id), | ||
self.async_remove_and_unregister, | ||
) | ||
) | ||
|
||
async def async_will_remove_from_hass(self): | ||
"""Entity being removed from hass.""" | ||
self.roller.callback_unsubscribe(self.notify_update) | ||
|
||
@callback | ||
def notify_update(self): | ||
"""Write updated device state information.""" | ||
LOGGER.debug("Device update notification received: %s", self.name) | ||
self.async_write_ha_state() | ||
|
||
@property | ||
def should_poll(self): | ||
"""Report that Acmeda entities do not need polling.""" | ||
return False | ||
|
||
@property | ||
def unique_id(self): | ||
"""Return the unique ID of this roller.""" | ||
return self.roller.id | ||
|
||
@property | ||
def device_id(self): | ||
"""Return the ID of this roller.""" | ||
return self.roller.id | ||
|
||
@property | ||
def name(self): | ||
"""Return the name of roller.""" | ||
return self.roller.name | ||
|
||
@property | ||
def device_info(self): | ||
"""Return the device info.""" | ||
return { | ||
"identifiers": {(DOMAIN, self.unique_id)}, | ||
"name": self.roller.name, | ||
"manufacturer": "Rollease Acmeda", | ||
"via_device": (DOMAIN, self.roller.hub.id), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
"""Config flow for Rollease Acmeda Automate Pulse Hub.""" | ||
import asyncio | ||
from typing import Dict, Optional | ||
|
||
import aiopulse | ||
import async_timeout | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
|
||
from .const import DOMAIN # pylint: disable=unused-import | ||
|
||
|
||
class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a Acmeda config flow.""" | ||
|
||
VERSION = 1 | ||
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL | ||
|
||
def __init__(self): | ||
"""Initialize the config flow.""" | ||
self.discovered_hubs: Optional[Dict[str, aiopulse.Hub]] = None | ||
|
||
async def async_step_user(self, user_input=None): | ||
"""Handle a flow initialized by the user.""" | ||
if ( | ||
user_input is not None | ||
and self.discovered_hubs is not None | ||
# pylint: disable=unsupported-membership-test | ||
and user_input["id"] in self.discovered_hubs | ||
): | ||
# pylint: disable=unsubscriptable-object | ||
return await self.async_create(self.discovered_hubs[user_input["id"]]) | ||
|
||
# Already configured hosts | ||
already_configured = { | ||
entry.unique_id for entry in self._async_current_entries() | ||
} | ||
|
||
hubs = [] | ||
try: | ||
with async_timeout.timeout(5): | ||
async for hub in aiopulse.Hub.discover(): | ||
if hub.id not in already_configured: | ||
hubs.append(hub) | ||
except asyncio.TimeoutError: | ||
pass | ||
|
||
if len(hubs) == 0: | ||
return self.async_abort(reason="all_configured") | ||
|
||
if len(hubs) == 1: | ||
return await self.async_create(hubs[0]) | ||
|
||
self.discovered_hubs = {hub.id: hub for hub in hubs} | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=vol.Schema( | ||
{ | ||
vol.Required("id"): vol.In( | ||
{hub.id: f"{hub.id} {hub.host}" for hub in hubs} | ||
) | ||
} | ||
), | ||
) | ||
|
||
async def async_create(self, hub): | ||
"""Create the Acmeda Hub entry.""" | ||
await self.async_set_unique_id(hub.id, raise_on_progress=False) | ||
return self.async_create_entry(title=hub.id, data={"host": hub.host}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
"""Constants for the Rollease Acmeda Automate integration.""" | ||
import logging | ||
|
||
LOGGER = logging.getLogger(__package__) | ||
DOMAIN = "acmeda" | ||
|
||
ACMEDA_HUB_UPDATE = "acmeda_hub_update_{}" | ||
ACMEDA_ENTITY_REMOVE = "acmeda_entity_remove_{}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
"""Support for Acmeda Roller Blinds.""" | ||
from homeassistant.components.cover import ( | ||
ATTR_POSITION, | ||
SUPPORT_CLOSE, | ||
SUPPORT_CLOSE_TILT, | ||
SUPPORT_OPEN, | ||
SUPPORT_OPEN_TILT, | ||
SUPPORT_SET_POSITION, | ||
SUPPORT_SET_TILT_POSITION, | ||
SUPPORT_STOP, | ||
SUPPORT_STOP_TILT, | ||
CoverEntity, | ||
) | ||
from homeassistant.core import callback | ||
from homeassistant.helpers.dispatcher import async_dispatcher_connect | ||
|
||
from .base import AcmedaBase | ||
from .const import ACMEDA_HUB_UPDATE, DOMAIN | ||
from .helpers import async_add_acmeda_entities | ||
|
||
|
||
async def async_setup_entry(hass, config_entry, async_add_entities): | ||
"""Set up the Acmeda Rollers from a config entry.""" | ||
hub = hass.data[DOMAIN][config_entry.entry_id] | ||
|
||
current = set() | ||
|
||
@callback | ||
def async_add_acmeda_covers(): | ||
async_add_acmeda_entities( | ||
hass, AcmedaCover, config_entry, current, async_add_entities | ||
) | ||
|
||
hub.cleanup_callbacks.append( | ||
async_dispatcher_connect( | ||
hass, | ||
ACMEDA_HUB_UPDATE.format(config_entry.entry_id), | ||
async_add_acmeda_covers, | ||
) | ||
) | ||
|
||
|
||
class AcmedaCover(AcmedaBase, CoverEntity): | ||
"""Representation of a Acmeda cover device.""" | ||
|
||
@property | ||
def current_cover_position(self): | ||
"""Return the current position of the roller blind. | ||
None is unknown, 0 is closed, 100 is fully open. | ||
""" | ||
position = None | ||
if self.roller.type != 7: | ||
position = 100 - self.roller.closed_percent | ||
return position | ||
|
||
@property | ||
def current_cover_tilt_position(self): | ||
"""Return the current tilt of the roller blind. | ||
None is unknown, 0 is closed, 100 is fully open. | ||
""" | ||
position = None | ||
if self.roller.type == 7 or self.roller.type == 10: | ||
position = 100 - self.roller.closed_percent | ||
return position | ||
|
||
@property | ||
def supported_features(self): | ||
"""Flag supported features.""" | ||
supported_features = 0 | ||
if self.current_cover_position is not None: | ||
supported_features |= ( | ||
SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION | ||
) | ||
if self.current_cover_tilt_position is not None: | ||
supported_features |= ( | ||
SUPPORT_OPEN_TILT | ||
| SUPPORT_CLOSE_TILT | ||
| SUPPORT_STOP_TILT | ||
| SUPPORT_SET_TILT_POSITION | ||
) | ||
|
||
return supported_features | ||
|
||
@property | ||
def is_closed(self): | ||
"""Return if the cover is closed.""" | ||
is_closed = self.roller.closed_percent == 100 | ||
return is_closed | ||
|
||
async def close_cover(self, **kwargs): | ||
"""Close the roller.""" | ||
await self.roller.move_down() | ||
|
||
async def open_cover(self, **kwargs): | ||
"""Open the roller.""" | ||
await self.roller.move_up() | ||
|
||
async def stop_cover(self, **kwargs): | ||
"""Stop the roller.""" | ||
await self.roller.move_stop() | ||
|
||
async def set_cover_position(self, **kwargs): | ||
"""Move the roller shutter to a specific position.""" | ||
await self.roller.move_to(100 - kwargs[ATTR_POSITION]) | ||
|
||
async def close_cover_tilt(self, **kwargs): | ||
"""Close the roller.""" | ||
await self.roller.move_down() | ||
|
||
async def open_cover_tilt(self, **kwargs): | ||
"""Open the roller.""" | ||
await self.roller.move_up() | ||
|
||
async def stop_cover_tilt(self, **kwargs): | ||
"""Stop the roller.""" | ||
await self.roller.move_stop() | ||
|
||
async def set_cover_tilt(self, **kwargs): | ||
"""Tilt the roller shutter to a specific position.""" | ||
await self.roller.move_to(100 - kwargs[ATTR_POSITION]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"""Errors for the Acmeda Pulse component.""" | ||
from homeassistant.exceptions import HomeAssistantError | ||
|
||
|
||
class PulseException(HomeAssistantError): | ||
"""Base class for Acmeda Pulse exceptions.""" | ||
|
||
|
||
class CannotConnect(PulseException): | ||
"""Unable to connect to the bridge.""" |
Oops, something went wrong.