mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Add Acmeda integration (#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.
This commit is contained in:
parent
eec1b3e7a7
commit
65e509ed8f
@ -10,6 +10,14 @@ omit =
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/acer_projector/switch.py
|
||||
homeassistant/components/actiontec/device_tracker.py
|
||||
homeassistant/components/acmeda/__init__.py
|
||||
homeassistant/components/acmeda/base.py
|
||||
homeassistant/components/acmeda/const.py
|
||||
homeassistant/components/acmeda/cover.py
|
||||
homeassistant/components/acmeda/errors.py
|
||||
homeassistant/components/acmeda/helpers.py
|
||||
homeassistant/components/acmeda/hub.py
|
||||
homeassistant/components/acmeda/sensor.py
|
||||
homeassistant/components/adguard/__init__.py
|
||||
homeassistant/components/adguard/const.py
|
||||
homeassistant/components/adguard/sensor.py
|
||||
|
@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
# Integrations
|
||||
homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
homeassistant/components/airly/* @bieniu
|
||||
|
59
homeassistant/components/acmeda/__init__.py
Normal file
59
homeassistant/components/acmeda/__init__.py
Normal file
@ -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
|
89
homeassistant/components/acmeda/base.py
Normal file
89
homeassistant/components/acmeda/base.py
Normal file
@ -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),
|
||||
}
|
71
homeassistant/components/acmeda/config_flow.py
Normal file
71
homeassistant/components/acmeda/config_flow.py
Normal file
@ -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})
|
8
homeassistant/components/acmeda/const.py
Normal file
8
homeassistant/components/acmeda/const.py
Normal file
@ -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_{}"
|
122
homeassistant/components/acmeda/cover.py
Normal file
122
homeassistant/components/acmeda/cover.py
Normal file
@ -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])
|
10
homeassistant/components/acmeda/errors.py
Normal file
10
homeassistant/components/acmeda/errors.py
Normal file
@ -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."""
|
41
homeassistant/components/acmeda/helpers.py
Normal file
41
homeassistant/components/acmeda/helpers.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Helper functions for Acmeda Pulse."""
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_acmeda_entities(
|
||||
hass, entity_class, config_entry, current, async_add_entities
|
||||
):
|
||||
"""Add any new entities."""
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
LOGGER.debug("Looking for new %s on: %s", entity_class.__name__, hub.host)
|
||||
|
||||
api = hub.api.rollers
|
||||
|
||||
new_items = []
|
||||
for unique_id, roller in api.items():
|
||||
if unique_id not in current:
|
||||
LOGGER.debug("New %s %s", entity_class.__name__, unique_id)
|
||||
new_item = entity_class(roller)
|
||||
current.add(unique_id)
|
||||
new_items.append(new_item)
|
||||
|
||||
async_add_entities(new_items)
|
||||
|
||||
|
||||
async def update_devices(hass, config_entry, api):
|
||||
"""Tell hass that device info has been updated."""
|
||||
dev_registry = await get_dev_reg(hass)
|
||||
|
||||
for api_item in api.values():
|
||||
# Update Device name
|
||||
device = dev_registry.async_get_device(
|
||||
identifiers={(DOMAIN, api_item.id)}, connections=set()
|
||||
)
|
||||
if device is not None:
|
||||
dev_registry.async_update_device(
|
||||
device.id, name=api_item.name,
|
||||
)
|
88
homeassistant/components/acmeda/hub.py
Normal file
88
homeassistant/components/acmeda/hub.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""Code to handle a Pulse Hub."""
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import aiopulse
|
||||
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import ACMEDA_ENTITY_REMOVE, ACMEDA_HUB_UPDATE, LOGGER
|
||||
from .helpers import update_devices
|
||||
|
||||
|
||||
class PulseHub:
|
||||
"""Manages a single Pulse Hub."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize the system."""
|
||||
self.config_entry = config_entry
|
||||
self.hass = hass
|
||||
self.api: Optional[aiopulse.Hub] = None
|
||||
self.tasks = []
|
||||
self.current_rollers = {}
|
||||
self.cleanup_callbacks = []
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
"""Return the title of the hub shown in the integrations list."""
|
||||
return f"{self.api.id} ({self.api.host})"
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
"""Return the host of this hub."""
|
||||
return self.config_entry.data["host"]
|
||||
|
||||
async def async_setup(self, tries=0):
|
||||
"""Set up a hub based on host parameter."""
|
||||
host = self.host
|
||||
|
||||
hub = aiopulse.Hub(host)
|
||||
self.api = hub
|
||||
|
||||
hub.callback_subscribe(self.async_notify_update)
|
||||
self.tasks.append(asyncio.create_task(hub.run()))
|
||||
|
||||
LOGGER.debug("Hub setup complete")
|
||||
return True
|
||||
|
||||
async def async_reset(self):
|
||||
"""Reset this hub to default state."""
|
||||
|
||||
for cleanup_callback in self.cleanup_callbacks:
|
||||
cleanup_callback()
|
||||
|
||||
# If not setup
|
||||
if self.api is None:
|
||||
return False
|
||||
|
||||
self.api.callback_unsubscribe(self.async_notify_update)
|
||||
await self.api.stop()
|
||||
del self.api
|
||||
self.api = None
|
||||
|
||||
# Wait for any running tasks to complete
|
||||
await asyncio.wait(self.tasks)
|
||||
|
||||
return True
|
||||
|
||||
async def async_notify_update(self, update_type):
|
||||
"""Evaluate entities when hub reports that update has occurred."""
|
||||
LOGGER.debug("Hub {update_type.name} updated")
|
||||
|
||||
if update_type == aiopulse.UpdateType.rollers:
|
||||
await update_devices(self.hass, self.config_entry, self.api.rollers)
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry, title=self.title
|
||||
)
|
||||
|
||||
async_dispatcher_send(
|
||||
self.hass, ACMEDA_HUB_UPDATE.format(self.config_entry.entry_id)
|
||||
)
|
||||
|
||||
for unique_id in list(self.current_rollers):
|
||||
if unique_id not in self.api.rollers:
|
||||
LOGGER.debug("Notifying remove of %s", unique_id)
|
||||
self.current_rollers.pop(unique_id)
|
||||
async_dispatcher_send(
|
||||
self.hass, ACMEDA_ENTITY_REMOVE.format(unique_id)
|
||||
)
|
10
homeassistant/components/acmeda/manifest.json
Normal file
10
homeassistant/components/acmeda/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "acmeda",
|
||||
"name": "Rollease Acmeda Automate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/acmeda",
|
||||
"requirements": ["aiopulse==0.4.0"],
|
||||
"codeowners": [
|
||||
"@atmurray"
|
||||
]
|
||||
}
|
46
homeassistant/components/acmeda/sensor.py
Normal file
46
homeassistant/components/acmeda/sensor.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Support for Acmeda Roller Blind Batteries."""
|
||||
from homeassistant.const import DEVICE_CLASS_BATTERY, UNIT_PERCENTAGE
|
||||
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_sensors():
|
||||
async_add_acmeda_entities(
|
||||
hass, AcmedaBattery, 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_sensors,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class AcmedaBattery(AcmedaBase):
|
||||
"""Representation of a Acmeda cover device."""
|
||||
|
||||
device_class = DEVICE_CLASS_BATTERY
|
||||
unit_of_measurement = UNIT_PERCENTAGE
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of roller."""
|
||||
return f"{super().name} Battery"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
return self.roller.battery
|
16
homeassistant/components/acmeda/strings.json
Normal file
16
homeassistant/components/acmeda/strings.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Rollease Acmeda Automate",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Pick a hub to add",
|
||||
"data": {
|
||||
"id": "Host ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"all_configured": "No new Pulse hubs discovered."
|
||||
}
|
||||
}
|
||||
}
|
16
homeassistant/components/acmeda/translations/en.json
Normal file
16
homeassistant/components/acmeda/translations/en.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"title": "Rollease Acmeda Automate",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Pick a hub to add",
|
||||
"data": {
|
||||
"id": "Host ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"all_configured": "No new Pulse hubs discovered."
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
FLOWS = [
|
||||
"abode",
|
||||
"acmeda",
|
||||
"adguard",
|
||||
"agent_dvr",
|
||||
"airly",
|
||||
|
@ -205,6 +205,9 @@ aionotify==0.2.0
|
||||
# homeassistant.components.notion
|
||||
aionotion==1.1.0
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.0
|
||||
|
||||
# homeassistant.components.hunterdouglas_powerview
|
||||
aiopvapi==1.6.14
|
||||
|
||||
|
@ -94,6 +94,9 @@ aiohue==2.1.0
|
||||
# homeassistant.components.notion
|
||||
aionotion==1.1.0
|
||||
|
||||
# homeassistant.components.acmeda
|
||||
aiopulse==0.4.0
|
||||
|
||||
# homeassistant.components.hunterdouglas_powerview
|
||||
aiopvapi==1.6.14
|
||||
|
||||
|
1
tests/components/acmeda/__init__.py
Normal file
1
tests/components/acmeda/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Rollease Acmeda Automate integration."""
|
143
tests/components/acmeda/test_config_flow.py
Normal file
143
tests/components/acmeda/test_config_flow.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""Define tests for the Acmeda config flow."""
|
||||
import aiopulse
|
||||
from asynctest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.acmeda.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DUMMY_HOST1 = "127.0.0.1"
|
||||
DUMMY_HOST2 = "127.0.0.2"
|
||||
|
||||
CONFIG = {
|
||||
CONF_HOST: DUMMY_HOST1,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hub_discover():
|
||||
"""Mock the hub discover method."""
|
||||
with patch("aiopulse.Hub.discover") as mock_discover:
|
||||
yield mock_discover
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hub_run():
|
||||
"""Mock the hub run method."""
|
||||
with patch("aiopulse.Hub.run") as mock_run:
|
||||
yield mock_run
|
||||
|
||||
|
||||
async def async_generator(items):
|
||||
"""Async yields items provided in a list."""
|
||||
for item in items:
|
||||
yield item
|
||||
|
||||
|
||||
async def test_show_form_no_hubs(hass, mock_hub_discover):
|
||||
"""Test that flow aborts if no hubs are discovered."""
|
||||
mock_hub_discover.return_value = async_generator([])
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "all_configured"
|
||||
|
||||
# Check we performed the discovery
|
||||
assert len(mock_hub_discover.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run):
|
||||
"""Test that a config is created when one hub discovered."""
|
||||
|
||||
dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1)
|
||||
dummy_hub_1.id = "ABC123"
|
||||
|
||||
mock_hub_discover.return_value = async_generator([dummy_hub_1])
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == dummy_hub_1.id
|
||||
assert result["result"].data == {
|
||||
"host": DUMMY_HOST1,
|
||||
}
|
||||
|
||||
# Check we performed the discovery
|
||||
assert len(mock_hub_discover.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_show_form_two_hubs(hass, mock_hub_discover):
|
||||
"""Test that the form is served when more than one hub discovered."""
|
||||
|
||||
dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1)
|
||||
dummy_hub_1.id = "ABC123"
|
||||
|
||||
dummy_hub_2 = aiopulse.Hub(DUMMY_HOST1)
|
||||
dummy_hub_2.id = "DEF456"
|
||||
|
||||
mock_hub_discover.return_value = async_generator([dummy_hub_1, dummy_hub_2])
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# Check we performed the discovery
|
||||
assert len(mock_hub_discover.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover):
|
||||
"""Test that a config is created when a second hub is discovered."""
|
||||
|
||||
dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1)
|
||||
dummy_hub_1.id = "ABC123"
|
||||
|
||||
dummy_hub_2 = aiopulse.Hub(DUMMY_HOST2)
|
||||
dummy_hub_2.id = "DEF456"
|
||||
|
||||
mock_hub_discover.return_value = async_generator([dummy_hub_1, dummy_hub_2])
|
||||
|
||||
MockConfigEntry(domain=DOMAIN, unique_id=dummy_hub_1.id, data=CONFIG).add_to_hass(
|
||||
hass
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == dummy_hub_2.id
|
||||
assert result["result"].data == {
|
||||
"host": DUMMY_HOST2,
|
||||
}
|
||||
|
||||
|
||||
async def test_already_configured(hass, mock_hub_discover):
|
||||
"""Test that flow aborts when all hubs are configured."""
|
||||
|
||||
dummy_hub_1 = aiopulse.Hub(DUMMY_HOST1)
|
||||
dummy_hub_1.id = "ABC123"
|
||||
|
||||
mock_hub_discover.return_value = async_generator([dummy_hub_1])
|
||||
|
||||
MockConfigEntry(domain=DOMAIN, unique_id=dummy_hub_1.id, data=CONFIG).add_to_hass(
|
||||
hass
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "all_configured"
|
Loading…
x
Reference in New Issue
Block a user