-
-
Notifications
You must be signed in to change notification settings - Fork 30.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new integration for WMS WebControl pro using local API (#124176)
* Add new integration for WMS WebControl pro using local API Warema recently released a new local API for their WMS hub called "WebControl pro". This integration makes use of the new local API via a new dedicated Python library pywmspro. For now this integration only supports awnings as covers. But pywmspro is device-agnostic to ease future extensions. * Incorporated review feedback from joostlek Thanks a lot! * Incorporated more review feedback from joostlek Thanks a lot! * Incorporated more review feedback from joostlek Thanks a lot! * Fix * Follow-up fix * Improve handling of DHCP discovery * Further test improvements suggested by joostlek, thanks! --------- Co-authored-by: Joostlek <[email protected]>
- Loading branch information
Showing
22 changed files
with
1,194 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
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,66 @@ | ||
"""The WMS WebControl pro API integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import aiohttp | ||
from wmspro.webcontrol import WebControlPro | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_HOST, Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers import device_registry as dr | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.typing import UNDEFINED | ||
|
||
from .const import DOMAIN, MANUFACTURER | ||
|
||
PLATFORMS: list[Platform] = [Platform.COVER] | ||
|
||
type WebControlProConfigEntry = ConfigEntry[WebControlPro] | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, entry: WebControlProConfigEntry | ||
) -> bool: | ||
"""Set up wmspro from a config entry.""" | ||
host = entry.data[CONF_HOST] | ||
session = async_get_clientsession(hass) | ||
hub = WebControlPro(host, session) | ||
|
||
try: | ||
await hub.ping() | ||
except aiohttp.ClientError as err: | ||
raise ConfigEntryNotReady(f"Error while connecting to {host}") from err | ||
|
||
entry.runtime_data = hub | ||
|
||
device_registry = dr.async_get(hass) | ||
device_registry.async_get_or_create( | ||
config_entry_id=entry.entry_id, | ||
connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} | ||
if entry.unique_id | ||
else UNDEFINED, | ||
identifiers={(DOMAIN, entry.entry_id)}, | ||
manufacturer=MANUFACTURER, | ||
model="WMS WebControl pro", | ||
configuration_url=f"http://{hub.host}/system", | ||
) | ||
|
||
try: | ||
await hub.refresh() | ||
for dest in hub.dests.values(): | ||
await dest.refresh() | ||
except aiohttp.ClientError as err: | ||
raise ConfigEntryNotReady(f"Error while refreshing from {host}") from err | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry( | ||
hass: HomeAssistant, entry: WebControlProConfigEntry | ||
) -> bool: | ||
"""Unload a config entry.""" | ||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
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 @@ | ||
"""Config flow for WMS WebControl pro API integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
|
||
import aiohttp | ||
import voluptuous as vol | ||
from wmspro.webcontrol import WebControlPro | ||
|
||
from homeassistant.components import dhcp | ||
from homeassistant.components.dhcp import DhcpServiceInfo | ||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
from homeassistant.const import CONF_HOST | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.device_registry import format_mac | ||
|
||
from .const import DOMAIN, SUGGESTED_HOST | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST): str, | ||
} | ||
) | ||
|
||
|
||
class WebControlProConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for wmspro.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_dhcp( | ||
self, discovery_info: dhcp.DhcpServiceInfo | ||
) -> ConfigFlowResult: | ||
"""Handle the DHCP discovery step.""" | ||
unique_id = format_mac(discovery_info.macaddress) | ||
await self.async_set_unique_id(unique_id) | ||
self._abort_if_unique_id_configured() | ||
|
||
for entry in self.hass.config_entries.async_entries(DOMAIN): | ||
if not entry.unique_id and entry.data[CONF_HOST] in ( | ||
discovery_info.hostname, | ||
discovery_info.ip, | ||
): | ||
self.hass.config_entries.async_update_entry(entry, unique_id=unique_id) | ||
return self.async_abort(reason="already_configured") | ||
|
||
return await self.async_step_user() | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle the user-based step.""" | ||
errors: dict[str, str] = {} | ||
if user_input is not None: | ||
self._async_abort_entries_match(user_input) | ||
host = user_input[CONF_HOST] | ||
session = async_get_clientsession(self.hass) | ||
hub = WebControlPro(host, session) | ||
try: | ||
pong = await hub.ping() | ||
except aiohttp.ClientError: | ||
errors["base"] = "cannot_connect" | ||
except Exception: | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
if not pong: | ||
errors["base"] = "cannot_connect" | ||
else: | ||
return self.async_create_entry(title=host, data=user_input) | ||
|
||
if self.source == dhcp.DOMAIN: | ||
discovery_info: DhcpServiceInfo = self.init_data | ||
data_values = {CONF_HOST: discovery_info.hostname or discovery_info.ip} | ||
else: | ||
data_values = {CONF_HOST: SUGGESTED_HOST} | ||
|
||
self.context["title_placeholders"] = data_values | ||
data_schema = self.add_suggested_values_to_schema( | ||
STEP_USER_DATA_SCHEMA, data_values | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=data_schema, errors=errors | ||
) |
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,7 @@ | ||
"""Constants for the WMS WebControl pro API integration.""" | ||
|
||
DOMAIN = "wmspro" | ||
SUGGESTED_HOST = "webcontrol" | ||
|
||
ATTRIBUTION = "Data provided by WMS WebControl pro API" | ||
MANUFACTURER = "WAREMA Renkhoff SE" |
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,77 @@ | ||
"""Support for covers connected with WMS WebControl pro.""" | ||
|
||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
from typing import Any | ||
|
||
from wmspro.const import ( | ||
WMS_WebControl_pro_API_actionDescription, | ||
WMS_WebControl_pro_API_actionType, | ||
) | ||
|
||
from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
|
||
from . import WebControlProConfigEntry | ||
from .entity import WebControlProGenericEntity | ||
|
||
SCAN_INTERVAL = timedelta(seconds=5) | ||
PARALLEL_UPDATES = 1 | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: WebControlProConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Set up the WMS based covers from a config entry.""" | ||
hub = config_entry.runtime_data | ||
|
||
entities: list[WebControlProGenericEntity] = [] | ||
for dest in hub.dests.values(): | ||
if dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive): | ||
entities.append(WebControlProAwning(config_entry.entry_id, dest)) # noqa: PERF401 | ||
|
||
async_add_entities(entities) | ||
|
||
|
||
class WebControlProAwning(WebControlProGenericEntity, CoverEntity): | ||
"""Representation of a WMS based awning.""" | ||
|
||
_attr_device_class = CoverDeviceClass.AWNING | ||
|
||
@property | ||
def current_cover_position(self) -> int | None: | ||
"""Return current position of cover.""" | ||
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive) | ||
return action["percentage"] | ||
|
||
async def async_set_cover_position(self, **kwargs: Any) -> None: | ||
"""Move the cover to a specific position.""" | ||
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive) | ||
await action(percentage=kwargs[ATTR_POSITION]) | ||
|
||
@property | ||
def is_closed(self) -> bool | None: | ||
"""Return if the cover is closed.""" | ||
return self.current_cover_position == 0 | ||
|
||
async def async_open_cover(self, **kwargs: Any) -> None: | ||
"""Open the cover.""" | ||
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive) | ||
await action(percentage=100) | ||
|
||
async def async_close_cover(self, **kwargs: Any) -> None: | ||
"""Close the cover.""" | ||
action = self._dest.action(WMS_WebControl_pro_API_actionDescription.AwningDrive) | ||
await action(percentage=0) | ||
|
||
async def async_stop_cover(self, **kwargs: Any) -> None: | ||
"""Stop the device if in motion.""" | ||
action = self._dest.action( | ||
WMS_WebControl_pro_API_actionDescription.ManualCommand, | ||
WMS_WebControl_pro_API_actionType.Stop, | ||
) | ||
await action() |
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,43 @@ | ||
"""Generic entity for the WMS WebControl pro API integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from wmspro.destination import Destination | ||
|
||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.entity import Entity | ||
|
||
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER | ||
|
||
|
||
class WebControlProGenericEntity(Entity): | ||
"""Foundation of all WMS based entities.""" | ||
|
||
_attr_attribution = ATTRIBUTION | ||
_attr_has_entity_name = True | ||
_attr_name = None | ||
|
||
def __init__(self, config_entry_id: str, dest: Destination) -> None: | ||
"""Initialize the entity with destination channel.""" | ||
dest_id_str = str(dest.id) | ||
self._dest = dest | ||
self._attr_unique_id = dest_id_str | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, dest_id_str)}, | ||
manufacturer=MANUFACTURER, | ||
model=dest.animationType.name, | ||
name=dest.name, | ||
serial_number=dest_id_str, | ||
suggested_area=dest.room.name, | ||
via_device=(DOMAIN, config_entry_id), | ||
configuration_url=f"http://{dest.host}/control", | ||
) | ||
|
||
async def async_update(self) -> None: | ||
"""Update the entity.""" | ||
await self._dest.refresh() | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return if entity is available.""" | ||
return self._dest.available |
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,19 @@ | ||
{ | ||
"domain": "wmspro", | ||
"name": "WMS WebControl pro", | ||
"codeowners": ["@mback2k"], | ||
"config_flow": true, | ||
"dependencies": [], | ||
"dhcp": [ | ||
{ | ||
"macaddress": "0023D5*" | ||
}, | ||
{ | ||
"registered_devices": true | ||
} | ||
], | ||
"documentation": "https://www.home-assistant.io/integrations/wmspro", | ||
"integration_type": "hub", | ||
"iot_class": "local_polling", | ||
"requirements": ["pywmspro==0.1.0"] | ||
} |
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,25 @@ | ||
{ | ||
"config": { | ||
"flow_title": "{host}", | ||
"step": { | ||
"user": { | ||
"data": { | ||
"host": "[%key:common::config_flow::data::host%]" | ||
}, | ||
"data_description": { | ||
"host": "The hostname or IP address of your WMS WebControl pro." | ||
} | ||
} | ||
}, | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]", | ||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
} | ||
} | ||
} |
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 |
---|---|---|
|
@@ -670,6 +670,7 @@ | |
"withings", | ||
"wiz", | ||
"wled", | ||
"wmspro", | ||
"wolflink", | ||
"workday", | ||
"worldclock", | ||
|
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
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
Oops, something went wrong.