Skip to content

Commit

Permalink
Add german translation, OptionsFlow and check whether dosage relays a…
Browse files Browse the repository at this point in the history
…re active
  • Loading branch information
ylabonte committed Aug 18, 2023
1 parent a6182a0 commit fe3d795
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 35 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ __pycache__

# misc
.coverage
.vscode
coverage.xml


Expand Down
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Home Assistant",
"type": "python",
"request": "launch",
"module": "homeassistant",
"justMyCode": false,
"args": [
"--debug",
"-c",
"config"
]
}
]
}
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
5 changes: 5 additions & 0 deletions config/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ logger:
default: info
logs:
custom_components.proconip_pool_controller: debug

# Enable VSCode debugging
debugpy:
start: true
wait: true
17 changes: 8 additions & 9 deletions custom_components/proconip_pool_controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,19 @@
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][
entry.entry_id
] = coordinator = ProconipPoolControllerDataUpdateCoordinator(
hass.data[DOMAIN][entry.entry_id] = ProconipPoolControllerDataUpdateCoordinator(
hass=hass,
client=ProconipApiClient(
base_url=entry.data[CONF_URL],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
base_url=entry.options[CONF_URL],
username=entry.options[CONF_USERNAME],
password=entry.options[CONF_PASSWORD],
hass=hass,
),
update_interval_in_seconds=entry.data[CONF_SCAN_INTERVAL],
update_interval_in_seconds=entry.options[CONF_SCAN_INTERVAL],
config_entry_id=entry.entry_id,
)
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
await coordinator.async_config_entry_first_refresh()
await hass.data[DOMAIN][entry.entry_id].async_config_entry_first_refresh()

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
Expand Down
8 changes: 5 additions & 3 deletions custom_components/proconip_pool_controller/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""ProCon.IP API Client."""
from __future__ import annotations

import aiohttp
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from proconip.definitions import (
ConfigObject,
Expand All @@ -23,13 +24,14 @@ def __init__(
base_url: str,
username: str,
password: str,
session: aiohttp.ClientSession,
hass: HomeAssistant,
) -> None:
"""ProCon.IP API Client."""
self.hass = hass
self._base_url = base_url
self._username = username
self._password = password
self._session = session
self._session = async_get_clientsession(hass)
self._api_config = ConfigObject(
base_url=self._base_url,
username=self._username,
Expand Down
128 changes: 124 additions & 4 deletions custom_components/proconip_pool_controller/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
CONF_URL,
CONF_USERNAME,
)
from homeassistant.core import (
callback,
HomeAssistant,
)
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from proconip.api import (
BadCredentialsException,
BadStatusCodeException,
Expand All @@ -31,10 +34,11 @@ async def async_step_user(
user_input: dict | None = None,
) -> config_entries.FlowResult:
"""Handle a flow initialized by the user."""
connection_tester = ProconipPoolControllerConnectionTester(self.hass)
_errors = {}
if user_input is not None:
try:
await self._test_credentials(
await connection_tester.async_test_credentials(
url=user_input[CONF_URL],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
Expand Down Expand Up @@ -94,15 +98,131 @@ async def async_step_user(
),
}
),
description_placeholders={},
errors=_errors,
)

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return ProconipPoolControllerOptionsFlowHandler(config_entry)


class ProconipPoolControllerOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for this integration."""

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

async def async_step_init(self, user_input=None):
"""Forward to step user."""
return await self.async_step_user(user_input)

async def async_step_user(
self, user_input: dict | None = None
) -> config_entries.FlowResult:
"""Handle device options."""
connection_tester = ProconipPoolControllerConnectionTester(self.hass)
_errors = {}
if user_input is not None:
try:
await connection_tester.async_test_credentials(
url=user_input[CONF_URL],
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
except BadCredentialsException as exception:
LOGGER.warning(exception)
_errors["base"] = "auth"
except BadStatusCodeException as exception:
LOGGER.error(exception)
_errors["base"] = "connection"
except ProconipApiException as exception:
LOGGER.exception(exception)
_errors["base"] = "unknown"
else:
self.options.update(user_input)
return self.async_create_entry(
title=user_input[CONF_URL],
data=self.options,
)

return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_URL,
default=(user_input or self.config_entry.options).get(
CONF_URL,
self.config_entry.data.get(CONF_URL)
),
): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.URL),
),
vol.Required(
CONF_SCAN_INTERVAL,
default=(user_input or self.config_entry.options).get(
CONF_SCAN_INTERVAL,
self.config_entry.data.get(CONF_SCAN_INTERVAL)
),
): selector.NumberSelector(
selector.NumberSelectorConfig(
mode=selector.NumberSelectorMode.SLIDER,
min=1,
max=60,
step=0.5,
),
),
vol.Required(
CONF_USERNAME,
default=(user_input or self.config_entry.options).get(
CONF_USERNAME,
self.config_entry.data.get(CONF_USERNAME)
),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT
),
),
vol.Required(
CONF_PASSWORD,
default=(user_input or self.config_entry.options).get(
CONF_PASSWORD,
self.config_entry.data.get(CONF_PASSWORD)
),
): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.PASSWORD
),
),
}
),
description_placeholders={},
errors=_errors,
)

async def _test_credentials(self, url: str, username: str, password: str) -> None:

class ProconipPoolControllerConnectionTester:
"""Helper class for connection testing."""

def __init__(self, hass: HomeAssistant) -> None:
"""Initialize connection tester."""
self.hass = hass

async def async_test_credentials(
self, url: str, username: str, password: str
) -> None:
"""Validate base url and credentials."""
client = ProconipApiClient(
base_url=url,
username=username,
password=password,
session=async_create_clientsession(self.hass),
hass=self.hass,
)
await client.async_get_data()
2 changes: 1 addition & 1 deletion custom_components/proconip_pool_controller/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

NAME = "ProCon.IP Pool Controller"
DOMAIN = "proconip_pool_controller"
VERSION = "1.0.2"
VERSION = "1.1.0"
ATTRIBUTION = (
"Data provided by your Pool Digital ProCon.IP (https://www.pooldigital.de)"
)
40 changes: 37 additions & 3 deletions custom_components/proconip_pool_controller/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,66 @@


# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
class ProconipPoolControllerDataUpdateCoordinator(DataUpdateCoordinator):
class ProconipPoolControllerDataUpdateCoordinator(DataUpdateCoordinator[GetStateData]):
"""Class to manage fetching data from the API."""

config_entry: ConfigEntry
config_entry_id: str
data: GetStateData

_active_dosage_relays = {}

def __init__(
self,
hass: HomeAssistant,
client: ProconipApiClient,
update_interval_in_seconds: float,
config_entry_id: str,
) -> None:
"""Initialize."""
self.client = client
self.config_entry_id = config_entry_id
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=update_interval_in_seconds),
update_method=self.proconip_update_method,
)

async def _async_update_data(self) -> GetStateData:
async def proconip_update_method(self) -> GetStateData:
"""Update data via library."""
data: GetStateData = None
try:
return await self.client.async_get_data()
data = await self.client.async_get_data()
except BadCredentialsException as exception:
raise ConfigEntryAuthFailed(exception) from exception
except (BadStatusCodeException, ProconipApiException) as exception:
raise UpdateFailed(exception) from exception

self._active_dosage_relays = {
data.chlorine_dosage_relay_id: data.is_chlorine_dosage_enabled(),
data.ph_minus_dosage_relay_id: data.is_ph_minus_dosage_enabled(),
data.ph_plus_dosage_relay_id: data.is_ph_plus_dosage_enabled(),
}

return data

def is_active_dosage_relay(self, relay_id) -> bool:
"""Return True if the given relay_id refers to an active dosage relay."""
if relay_id in self._active_dosage_relays:
return self._active_dosage_relays[relay_id]

return False

# @property
# def config_entry(self):
# """Property wrapping the _config_entry attribite."""
# return self._config_entry

# @config_entry.setter
# def config_entry(self, value):
# """Setter for _config_entry attribute, updating the update_interval attribute."""
# self._config_entry = value
# if value is not None and CONF_SCAN_INTERVAL in value.options:
# self.update_interval = timedelta(seconds=value.options[CONF_SCAN_INTERVAL])
6 changes: 3 additions & 3 deletions custom_components/proconip_pool_controller/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def __init__(
"""Initialize."""
super().__init__(coordinator)
if self._attr_unique_id is None:
self._attr_unique_id = coordinator.config_entry.entry_id
self._attr_unique_id = coordinator.config_entry_id
else:
self._attr_unique_id = (
f"{coordinator.config_entry.entry_id}-{self._attr_unique_id}"
f"{coordinator.config_entry_id}-{self._attr_unique_id}"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
identifiers={(DOMAIN, coordinator.config_entry_id)},
name=NAME,
model=VERSION,
manufacturer=NAME,
Expand Down
4 changes: 2 additions & 2 deletions custom_components/proconip_pool_controller/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/ylabonte/proconip-hass/issues",
"requirements": [
"proconip==1.2.7"
"proconip==1.3.0"
],
"version": "1.0.2"
"version": "1.1.0"
}
7 changes: 4 additions & 3 deletions custom_components/proconip_pool_controller/number.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
"""Switch platform for proconip."""
from __future__ import annotations

from homeassistant.components.number import NumberEntity
import asyncio

from homeassistant.components.number import NumberEntity

from .const import DOMAIN
from .coordinator import ProconipPoolControllerDataUpdateCoordinator
from .entity import ProconipPoolControllerEntity


async def async_setup_entry(hass, entry, async_add_devices):
"""Set up the select platform."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator: ProconipPoolControllerDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
number_of_relays = 16 if coordinator.data.is_relay_extension_enabled() else 8
relays = []
for i in range(number_of_relays):
if coordinator.data.is_dosage_relay(relay_id=i):
if coordinator.is_active_dosage_relay(relay_id=i):
relays.append(
ProconipPoolControllerDosageRelayTimer(
coordinator=coordinator,
Expand Down
Loading

0 comments on commit fe3d795

Please sign in to comment.