Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suez_water: dynamically create devices #135411

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions homeassistant/components/suez_water/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

import logging

from homeassistant.const import Platform
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr

from .const import CONF_COUNTER_ID
from .config_flow import validate_input
from .const import CONF_COUNTER_ID, DOMAIN
from .coordinator import SuezWaterConfigEntry, SuezWaterCoordinator

PLATFORMS: list[Platform] = [Platform.SENSOR]
Expand Down Expand Up @@ -48,12 +50,19 @@ async def async_migrate_entry(

if config_entry.version == 1:
# Migrate to version 2
counter_id = config_entry.data.get(CONF_COUNTER_ID)
unique_id = str(counter_id)
contract = await validate_input(
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
)
unique_id = str(contract.fullRefFormat)

data = config_entry.data.copy()
data.pop(CONF_COUNTER_ID)

hass.config_entries.async_update_entry(
config_entry,
unique_id=unique_id,
data=data,
version=2,
)

Expand All @@ -64,3 +73,17 @@ async def async_migrate_entry(
)

return True


async def async_remove_config_entry_device(
hass: HomeAssistant,
config_entry: SuezWaterConfigEntry,
device_entry: dr.DeviceEntry,
) -> bool:
"""Remove a config entry from a device."""
return not any(
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
and identifier[1] in config_entry.runtime_data.data.current_device_id
)
50 changes: 31 additions & 19 deletions homeassistant/components/suez_water/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,54 @@
import logging
from typing import Any

from pysuez import PySuezError, SuezClient
from pysuez import ContractResult, PySuezError, SuezClient
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.exceptions import HomeAssistantError

from .const import CONF_COUNTER_ID, DOMAIN
from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_COUNTER_ID): str,
}
)


async def validate_input(data: dict[str, Any]) -> None:
async def validate_input(username: str, password: str) -> ContractResult:
"""Validate the user input allows us to connect.

Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
try:
counter_id = data.get(CONF_COUNTER_ID)
client = SuezClient(
data[CONF_USERNAME],
data[CONF_PASSWORD],
counter_id,
)
client = SuezClient(username=username, password=password, counter_id=None)
try:
if not await client.check_credentials():
raise InvalidAuth
except PySuezError as ex:
raise CannotConnect from ex

if counter_id is None:
try:
data[CONF_COUNTER_ID] = await client.find_counter()
except PySuezError as ex:
raise CounterNotFound from ex
try:
contract = await client.contract_data()
if not contract.isCurrentContract:
raise NoActiveContract
except PySuezError as ex:
raise NoActiveContract from ex

try:
await client.find_counter()
except PySuezError as ex:
raise CounterNotFound from ex
finally:
await client.close_session()

return contract


class SuezWaterConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Suez Water."""
Expand All @@ -65,21 +67,27 @@ async def async_step_user(

if user_input is not None:
try:
await validate_input(user_input)
contract = await validate_input(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except CounterNotFound:
errors["base"] = "counter_not_found"
except NoActiveContract:
errors["base"] = "no_active_contract"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
counter_id = str(user_input[CONF_COUNTER_ID])
await self.async_set_unique_id(counter_id)
await self.async_set_unique_id(contract.fullRefFormat)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=counter_id, data=user_input)
return self.async_create_entry(
title=contract.fullRefFormat, data=user_input
)

return self.async_show_form(
step_id="user",
Expand All @@ -99,3 +107,7 @@ class InvalidAuth(HomeAssistantError):

class CounterNotFound(HomeAssistantError):
"""Error to indicate we failed to automatically find the counter id."""


class NoActiveContract(HomeAssistantError):
"""Error to indicate we failed to find an active contract."""
50 changes: 48 additions & 2 deletions homeassistant/components/suez_water/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import _LOGGER, HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import (
DeviceEntry,
DeviceEntryType,
DeviceInfo,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_COUNTER_ID, DATA_REFRESH_INTERVAL, DOMAIN
from .const import DATA_REFRESH_INTERVAL, DOMAIN


@dataclass
Expand All @@ -33,6 +39,8 @@ class SuezWaterData:
aggregated_value: float
aggregated_attr: SuezWaterAggregatedAttributes
price: float
new_device: None | DeviceInfo
current_device_id: str


type SuezWaterConfigEntry = ConfigEntry[SuezWaterCoordinator]
Expand All @@ -54,22 +62,32 @@ def __init__(self, hass: HomeAssistant, config_entry: SuezWaterConfigEntry) -> N
always_update=True,
config_entry=config_entry,
)
self.counter_id: None | str = None

async def _async_setup(self) -> None:
self._suez_client = SuezClient(
username=self.config_entry.data[CONF_USERNAME],
password=self.config_entry.data[CONF_PASSWORD],
counter_id=self.config_entry.data[CONF_COUNTER_ID],
counter_id=None,
)
if not await self._suez_client.check_credentials():
raise ConfigEntryError("Invalid credentials for suez water")
try:
await self._suez_client.find_counter()
except PySuezError as ex:
raise ConfigEntryError("Can't find counter id") from ex

async def _async_update_data(self) -> SuezWaterData:
"""Fetch data from API endpoint."""

def map_dict(param: dict[date, float]) -> dict[str, float]:
return {str(key): value for key, value in param.items()}

try:
new_device, counter_id = await self._get_device()
except PySuezError as err:
raise UpdateFailed("Suez data update failed to find counter") from err

try:
aggregated = await self._suez_client.fetch_aggregated_data()
data = SuezWaterData(
Expand All @@ -83,8 +101,36 @@ def map_dict(param: dict[date, float]) -> dict[str, float]:
history=map_dict(aggregated.history),
),
price=(await self._suez_client.get_price()).price,
new_device=new_device,
current_device_id=counter_id,
)
except PySuezError as err:
raise UpdateFailed(f"Suez data update failed: {err}") from err
_LOGGER.debug("Successfully fetched suez data")
return data

async def _get_device(self) -> tuple[None | DeviceInfo, str]:
counter_id = str(await self._suez_client.find_counter())

if counter_id != self.counter_id:
if self.counter_id:
device_registry = dr.async_get(self.hass)
device: None | DeviceEntry = device_registry.async_get_device(
identifiers={(DOMAIN, self.counter_id)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
self.counter_id = counter_id
new_device = DeviceInfo(
identifiers={(DOMAIN, counter_id)},
entry_type=DeviceEntryType.SERVICE,
manufacturer="Suez",
serial_number=counter_id,
)
else:
new_device = None

return new_device, counter_id
18 changes: 6 additions & 12 deletions homeassistant/components/suez_water/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ rules:
comment: no service action and coordinator updates
test-coverage: done
integration-owner: done
docs-installation-parameters:
status: todo
comment: missing user/password
docs-installation-parameters: done
docs-configuration-parameters:
status: exempt
comment: no configuration option
Expand All @@ -51,8 +49,8 @@ rules:
entity-translations: done
entity-device-class: done
devices:
status: todo
comment: see https://github.com/home-assistant/core/pull/134027#discussion_r1898732463
status: done
comment: create one device corresponding to given meter
entity-category:
status: done
comment: default class is fine
Expand All @@ -62,9 +60,7 @@ rules:
discovery:
status: exempt
comment: api only, nothing on local network to discover services
stale-devices:
status: todo
comment: see devices
stale-devices: done
diagnostics: todo
exception-translations: todo
icon-translations:
Expand All @@ -73,17 +69,15 @@ rules:
reconfiguration-flow:
status: todo
comment: reconfigure every configurations input
dynamic-devices:
status: todo
comment: see devices
dynamic-devices: done
discovery-update-info:
status: exempt
comment: devices are not network dependent and will not be updated during their lives
repair-issues:
status: exempt
comment: No repair issues to be raised
docs-use-cases: done
docs-supported-devices: todo
docs-supported-devices: done
docs-supported-functions: done
docs-data-update:
status: todo
Expand Down
30 changes: 17 additions & 13 deletions homeassistant/components/suez_water/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
)
from homeassistant.const import CURRENCY_EURO, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import CONF_COUNTER_ID, DOMAIN
from .coordinator import SuezWaterConfigEntry, SuezWaterCoordinator, SuezWaterData


Expand Down Expand Up @@ -57,11 +56,20 @@ async def async_setup_entry(
) -> None:
"""Set up Suez Water sensor from a config entry."""
coordinator = entry.runtime_data
counter_id = entry.data[CONF_COUNTER_ID]

async_add_entities(
SuezWaterSensor(coordinator, counter_id, description) for description in SENSORS
)
def _check_device(known_device_id: None | str = None) -> None:
current_device_id = coordinator.data.current_device_id
if coordinator.data.new_device and (
known_device_id is None or current_device_id != known_device_id
):
known_device_id = current_device_id
async_add_entities(
SuezWaterSensor(coordinator, coordinator.data.new_device, description)
for description in SENSORS
)

_check_device()
entry.async_on_unload(coordinator.async_add_listener(_check_device))


class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity):
Expand All @@ -74,17 +82,13 @@ class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity):
def __init__(
self,
coordinator: SuezWaterCoordinator,
counter_id: int,
device: DeviceInfo,
entity_description: SuezWaterSensorEntityDescription,
) -> None:
"""Initialize the suez water sensor entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{counter_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, str(counter_id))},
entry_type=DeviceEntryType.SERVICE,
manufacturer="Suez",
)
self._attr_unique_id = f"{device['serial_number']}_{entity_description.key}"
self._attr_device_info = device
self.entity_description = entity_description

@property
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/suez_water/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"counter_not_found": "Could not find meter id automatically"
"counter_not_found": "Could not find meter id automatically",
"no_active_contract": "No active contract could be found"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
Expand Down
Loading