Skip to content

Commit

Permalink
Merge pull request #59 from erikkastelec/dev
Browse files Browse the repository at this point in the history
Move to config flow setup
  • Loading branch information
erikkastelec authored Apr 24, 2023
2 parents bc6cd8d + 18d0f8b commit d633101
Show file tree
Hide file tree
Showing 15 changed files with 1,254 additions and 711 deletions.
35 changes: 10 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,24 @@ This is how your custom_components directory should look like:
custom_components
├── wemportal
│ ├── __init__.py
│ ├── const.py
│ ├── manifest.json
│ ├── coordinator.py
│ ├── select.py
│ ├── number.py
│ ├── sensor.py
│ ├── ...
│ ├── ...
│ ├── ...
│ └── wemportalapi.py
```

## Configuration

Integration must be configured in Home Assistant frontend: Go to `Settings > Devices&Services `, click on ` Add integration ` button and search for `Weishaupt WEM Portal`.

After Adding the integration, you can click `CONFIGURE` button to edit the default settings. Make sure to read what each setting does below.

Configuration variables:

- `username`: Email address used for logging into WEM Portal
- `password`: Password used for logging into WEM Portal
- `scan_interval (Optional)`: Defines update frequency of web scraping. Optional and in seconds (defaults to 30 min).
Setting update frequency bellow 15 min is not recommended.
Setting update frequency below 15 min is not recommended.
- `api_scan_interval (Optional)`: Defines update frequency for API data fetching. Optional and in seconds (defaults to 5
min, should not be lower than 3 min).
- `language (
Expand All @@ -53,24 +54,8 @@ Configuration variables:
mobile API. Option `web` gets only the data on the website, while option `both` queries website and api and provides
all the available data from both sources.

Add the following to your `configuration.yaml` file:

```yaml
# Example configuration.yaml entry
wemportal:
#scan_interval: 1800
#api_scan_interval: 300
#language: en
#mode: api
username: your_username
password: your_password
```

## Troubleshooting
Please set your logging for the custom_component to debug:
```yaml
logger:
default: warn
logs:
custom_components.wemportal: debug
```

Go to `Settings > Devices&Services `, find WEM Portal and click on `three dots` at the bottom of the card. Click on `Enable debug logging`.
149 changes: 114 additions & 35 deletions custom_components/wemportal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,138 @@
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType

from .const import CONF_LANGUAGE, CONF_MODE, CONF_SCAN_INTERVAL_API, DOMAIN, PLATFORMS
from homeassistant.config_entries import ConfigEntry
from .const import (
CONF_LANGUAGE,
CONF_MODE,
CONF_SCAN_INTERVAL_API,
DOMAIN,
PLATFORMS,
_LOGGER,
DEFAULT_CONF_SCAN_INTERVAL_API_VALUE,
DEFAULT_CONF_SCAN_INTERVAL_VALUE,
)
from .coordinator import WemPortalDataUpdateCoordinator
from .wemportalapi import WemPortalApi
import homeassistant.helpers.entity_registry as entity_registry

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Optional(
CONF_SCAN_INTERVAL, default=timedelta(minutes=30)
): config_validation.time_period,
vol.Optional(
CONF_SCAN_INTERVAL_API, default=timedelta(minutes=5)
): config_validation.time_period,
vol.Optional(CONF_LANGUAGE, default="en"): config_validation.string,
vol.Optional(CONF_MODE, default="api"): config_validation.string,
vol.Required(CONF_USERNAME): config_validation.string,
vol.Required(CONF_PASSWORD): config_validation.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)

def get_wemportal_unique_id(config_entry_id: str, device_id: str, name: str):
"""Return unique ID for WEM Portal."""
return f"{config_entry_id}:{device_id}:{name}"


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the wemportal component."""
hass.data.setdefault(DOMAIN, {})
return True


# Migrate values from previous versions
async def migrate_unique_ids(
hass: HomeAssistant, config_entry: ConfigEntry, coordinator
):
er = entity_registry.async_get(hass)
# Do migration for first device if we have multiple
device_id = list(coordinator.data.keys())[0]
data = coordinator.data[device_id]

change = False
for unique_id, values in data.items():
name_id = er.async_get_entity_id(values["platform"], DOMAIN, unique_id)
new_id = get_wemportal_unique_id(config_entry.entry_id, device_id, unique_id)
if name_id is not None:
_LOGGER.info(
f"Found entity with old id ({name_id}). Updating to new unique_id ({new_id})."
)
# check if there already is a new one
new_entity_id = er.async_get_entity_id(values["platform"], DOMAIN, new_id)
if new_entity_id is not None:
_LOGGER.info(
"Found entity with old id and an entity with a new unique_id. Preserving old entity..."
)
er.async_remove(new_entity_id)
er.async_update_entity(
name_id,
new_unique_id=new_id,
)
change = True
if change:
await coordinator.async_config_entry_first_refresh()


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the wemportal component."""
# Set proper update_interval, based on selected mode
if config[DOMAIN].get(CONF_MODE) == "web":
update_interval = config[DOMAIN].get(CONF_SCAN_INTERVAL)
if entry.options.get(CONF_MODE) == "web":
update_interval = entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_CONF_SCAN_INTERVAL_VALUE
)

elif config[DOMAIN].get(CONF_MODE) == "api":
update_interval = config[DOMAIN].get(CONF_SCAN_INTERVAL_API)
elif entry.options.get(CONF_MODE) == "api":
update_interval = entry.options.get(
CONF_SCAN_INTERVAL_API, DEFAULT_CONF_SCAN_INTERVAL_API_VALUE
)
else:
update_interval = min(
config[DOMAIN].get(CONF_SCAN_INTERVAL),
config[DOMAIN].get(CONF_SCAN_INTERVAL_API),
entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_CONF_SCAN_INTERVAL_VALUE),
entry.options.get(
CONF_SCAN_INTERVAL_API, DEFAULT_CONF_SCAN_INTERVAL_API_VALUE
),
)
# Creatie API object
api = WemPortalApi(config[DOMAIN])

api = WemPortalApi(
entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), entry.options
)
# Create custom coordinator
coordinator = WemPortalDataUpdateCoordinator(hass, api, update_interval)
coordinator = WemPortalDataUpdateCoordinator(
hass, api, entry, timedelta(seconds=update_interval)
)

await coordinator.async_config_entry_first_refresh()

# try:
# version = entry.version
# if version < 2:
# await migrate_unique_ids(hass, entry, coordinator)
# except Exception:
# await migrate_unique_ids(hass, entry, coordinator)

hass.data[DOMAIN] = {
# Is there an on_update function that we can add listener to?
_LOGGER.info("Migrating entity names for wemportal")
await migrate_unique_ids(hass, entry, coordinator)

hass.data[DOMAIN][entry.entry_id] = {
"api": api,
"config": config[DOMAIN],
# "config": entry.data,
"coordinator": coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_entry_updated))

await coordinator.async_config_entry_first_refresh()
return True

# Initialize platforms
for platform in PLATFORMS:
hass.helpers.discovery.load_platform(platform, DOMAIN, {}, config)

async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
return True


async def _async_entry_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle entry updates."""
_LOGGER.info("Migrating entity names for wemportal because of config entry update")
await migrate_unique_ids(
hass, config_entry, hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
)
await hass.config_entries.async_reload(config_entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Handle removal of an entry."""
unload_ok = bool(
await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)

return unload_ok
132 changes: 132 additions & 0 deletions custom_components/wemportal/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Config flow for wemportal integration."""
from __future__ import annotations

import logging

import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as config_validation
from .wemportalapi import WemPortalApi
from .const import (
DOMAIN,
CONF_LANGUAGE,
CONF_MODE,
CONF_SCAN_INTERVAL_API,
)
from .exceptions import AuthError, UnknownAuthError

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect."""

# Create API object
api = WemPortalApi(data[CONF_USERNAME], data[CONF_PASSWORD])

# Try to login
try:
await hass.async_add_executor_job(api.api_login)
except AuthError:
raise InvalidAuth from AuthError
except UnknownAuthError:
raise CannotConnect from UnknownAuthError

return data


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for wemportal."""

VERSION = 2

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> WemportalOptionsFlow:
"""Get the options flow for this handler."""
return WemportalOptionsFlow(config_entry)

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
for existing_entry in self._async_current_entries(include_ignore=False):
if existing_entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]:
return self.async_abort(reason="already_configured")

return self.async_create_entry(
title=info[CONF_USERNAME], data=user_input
)

except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""


class WemportalOptionsFlow(config_entries.OptionsFlow):
"""Handle options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 1800),
): config_validation.positive_int,
vol.Optional(
CONF_SCAN_INTERVAL_API,
default=self.config_entry.options.get(
CONF_SCAN_INTERVAL_API, 300
),
): config_validation.positive_int,
vol.Optional(
CONF_LANGUAGE,
default=self.config_entry.options.get(CONF_LANGUAGE, "en"),
): config_validation.string,
vol.Optional(
CONF_MODE,
default=self.config_entry.options.get(CONF_MODE, "api"),
): config_validation.string,
}
),
)
15 changes: 10 additions & 5 deletions custom_components/wemportal/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
from typing import Final

_LOGGER = logging.getLogger("custom_components.wemportal")
DOMAIN = "wemportal"
DEFAULT_NAME = "Weishaupt WEM Portal"
DEFAULT_TIMEOUT = 360
START_URLS = ["https://www.wemportal.com/Web/login.aspx"]
DOMAIN: Final = "wemportal"
DEFAULT_NAME: Final = "Weishaupt WEM Portal"
DEFAULT_TIMEOUT: Final = 360
START_URLS: Final = ["https://www.wemportal.com/Web/login.aspx"]
CONF_SCAN_INTERVAL_API: Final = "api_scan_interval"
CONF_LANGUAGE: Final = "language"
CONF_MODE: Final = "mode"
PLATFORMS = ["sensor", "number", "select", "switch"]
REFRESH_WAIT_TIME: int = 360
REFRESH_WAIT_TIME: Final = 360
DATA_GATHERING_ERROR: Final = "An error occurred while gathering data.This issue should resolve by itself. If this problem persists,open an issue at https://github.com/erikkastelec/hass-WEM-Portal/issues"
DEFAULT_CONF_SCAN_INTERVAL_API_VALUE: Final = 300
DEFAULT_CONF_SCAN_INTERVAL_VALUE: Final = 1800
DEFAULT_CONF_LANGUAGE_VALUE: Final = "en"
DEFAULT_CONF_MODE_VALUE: Final = "api"
Loading

0 comments on commit d633101

Please sign in to comment.