Skip to content
Closed
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
128 changes: 95 additions & 33 deletions custom_components/plugwise/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@
# Upstream basically the whole file (excluding the pw-beta options)


def base_schema(
cf_input: ZeroconfServiceInfo | dict[str, Any] | None,
SMILE_RECONF_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
}
)


def smile_user_schema(
configure_input: ZeroconfServiceInfo | dict[str, Any] | None,
) -> vol.Schema:
"""Generate base schema for gateways."""
if not cf_input: # no discovery- or user-input available
if configure_input is None: # no discovery- or user-input available
return vol.Schema(
{
vol.Required(CONF_HOST): str,
Expand All @@ -88,25 +96,49 @@ def base_schema(
}
)

if isinstance(cf_input, ZeroconfServiceInfo):
if isinstance(configure_input, ZeroconfServiceInfo):
return vol.Schema({vol.Required(CONF_PASSWORD): str})

# When an error occurs show the previously entered userdata
return vol.Schema(
{
vol.Required(CONF_HOST, default=cf_input[CONF_HOST]): str,
vol.Required(CONF_PASSWORD, default=cf_input[CONF_PASSWORD]): str,
vol.Optional(CONF_PORT, default=cf_input[CONF_PORT]): int,
vol.Required(CONF_USERNAME, default=cf_input[CONF_USERNAME]): vol.In(
vol.Required(CONF_HOST, default=configure_input[CONF_HOST]): str,
vol.Required(CONF_PASSWORD, default=configure_input[CONF_PASSWORD]): str,
vol.Optional(CONF_PORT, default=configure_input[CONF_PORT]): int,
vol.Required(CONF_USERNAME, default=configure_input[CONF_USERNAME]): vol.In(
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
),
}
)


async def verify_connection(
hass: HomeAssistant, user_input: dict[str, Any]
) -> Smile | dict[str, str]:
"""Verify and return the gateway connection using helper function."""
errors: dict[str, str] = {}

try:
return await validate_input(hass, user_input)
except ConnectionFailedError:
errors[CONF_BASE] = "cannot_connect"
except InvalidAuthentication:
errors[CONF_BASE] = "invalid_auth"
except InvalidSetupError:
errors[CONF_BASE] = "invalid_setup"
except (InvalidXMLError, ResponseError):
errors[CONF_BASE] = "response_error"
except UnsupportedDeviceError:
errors[CONF_BASE] = "unsupported"
except Exception: # noqa: BLE001
errors[CONF_BASE] = "unknown"
return errors


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
"""Validate whether the user input allows us to connect to the gateway.

Data has the keys from base_schema() with values provided by the user.
Data has the keys from smile_user_schema() with values provided by the user.
"""
websession = async_get_clientsession(hass, verify_ssl=False)
api = Smile(
Expand Down Expand Up @@ -201,40 +233,24 @@ async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step when using network/gateway setups."""
errors: dict[str, str] = {}

if not user_input:
if user_input is None:
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=base_schema(self.discovery_info),
errors=errors,
data_schema=smile_user_schema(self.discovery_info),
errors={},
)

if self.discovery_info:
user_input[CONF_HOST] = self.discovery_info.host
user_input[CONF_PORT] = self.discovery_info.port
user_input[CONF_USERNAME] = self._username

try:
api = await validate_input(self.hass, user_input)
except ConnectionFailedError:
errors[CONF_BASE] = "cannot_connect"
except InvalidAuthentication:
errors[CONF_BASE] = "invalid_auth"
except InvalidSetupError:
errors[CONF_BASE] = "invalid_setup"
except (InvalidXMLError, ResponseError):
errors[CONF_BASE] = "response_error"
except UnsupportedDeviceError:
errors[CONF_BASE] = "unsupported"
except Exception: # noqa: BLE001
errors[CONF_BASE] = "unknown"

if errors:
api = await verify_connection(self.hass, user_input)
if isinstance(api, dict): # error returned
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=base_schema(user_input),
errors=errors,
data_schema=smile_user_schema(user_input),
errors=api,
)

await self.async_set_unique_id(
Expand All @@ -243,6 +259,53 @@ async def async_step_user(
self._abort_if_unique_id_configured()
return self.async_create_entry(title=api.smile_name, data=user_input)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
reconfigure_entry = self._get_reconfigure_entry()

if user_input is None:
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
data_schema=SMILE_RECONF_SCHEMA,
suggested_values=reconfigure_entry.data,
),
description_placeholders={"title": reconfigure_entry.title},
errors={},
)

# Keep current username and password
full_input = {
CONF_HOST: user_input.get(CONF_HOST),
CONF_PORT: user_input.get(CONF_PORT),
CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
}

api = await verify_connection(self.hass, full_input)
if isinstance(api, dict): # error returned
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
data_schema=SMILE_RECONF_SCHEMA,
suggested_values=reconfigure_entry.data,
),
description_placeholders={"title": reconfigure_entry.title},
errors=api,
)

await self.async_set_unique_id(
api.smile_hostname or api.gateway_id, raise_on_progress=False
)
self._abort_if_unique_id_mismatch(reason="not_the_same_smile")
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=full_input,
)


@staticmethod
@callback
def async_get_options_flow(
Expand All @@ -251,7 +314,6 @@ def async_get_options_flow(
"""Get the options flow for this handler."""
return PlugwiseOptionsFlowHandler(config_entry)


# pw-beta - change the scan-interval via CONFIGURE
# pw-beta - add homekit emulation via CONFIGURE
# pw-beta - change the frontend refresh interval via CONFIGURE
Expand Down
11 changes: 10 additions & 1 deletion custom_components/plugwise/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
},
"config": {
"step": {
"reconfigure": {
"description": "Update configuration for {title}.",
"data": {
"host": "IP-address",
"port": "Port number"
}
},
"user": {
"title": "Set up Plugwise Adam/Smile/Stretch",
"description": "Enter your Plugwise device: (setup can take up to 90s)",
Expand All @@ -42,7 +49,9 @@
},
"abort": {
"already_configured": "This device is already configured",
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna"
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
"not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address",
"reconfigure_successful": "Reconfiguration successful"
}
},
"entity": {
Expand Down
11 changes: 10 additions & 1 deletion custom_components/plugwise/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
},
"config": {
"step": {
"reconfigure": {
"description": "Update configuration for {title}.",
"data": {
"host": "IP-address",
"port": "Port number"
}
},
"user": {
"title": "Set up Plugwise Adam/Smile/Stretch",
"description": "Enter your Plugwise device: (setup can take up to 90s)",
Expand All @@ -42,7 +49,9 @@
},
"abort": {
"already_configured": "This device is already configured",
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna"
"anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna",
"not_the_same_smile": "The configured Smile ID does not match the Smile ID on the requested IP address",
"reconfigure_successful": "Reconfiguration successful"
}
},
"entity": {
Expand Down
11 changes: 10 additions & 1 deletion custom_components/plugwise/translations/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
},
"config": {
"step": {
"reconfigure": {
"description": "Pas configuratie aan voor {title}.",
"data": {
"host": "IP-adres",
"port": "Poortnumber"
}
},
"user": {
"title": "Installeer Plugwise Adam/Smile/Stretch",
"description": "Van uw Plugwise apparaat voer in: (installeren kan tot 90s duren)",
Expand All @@ -42,7 +49,9 @@
},
"abort": {
"already_configured": "Dit apparaat is al geconfigureerd",
"anna_with_adam": "Zowel Anna als Adam gedetecteerd. Voeg alleen de Adam toe, niet de Anna"
"anna_with_adam": "Zowel Anna als Adam gedetecteerd. Voeg alleen de Adam toe, niet de Anna",
"not_the_same_smile": "Het ingegeven Smile ID past niet bij het Smile ID van het ingegeven IP adres",
"reconfigure_successful": "Herconfiguratie is gelukt"
}
},
"entity": {
Expand Down
12 changes: 9 additions & 3 deletions tests/components/plugwise/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,15 @@ def mock_smile_adam() -> Generator[MagicMock]:
"""Create a Mock Adam type for testing."""
chosen_env = "m_adam_multiple_devices_per_zone"
all_data = _read_json(chosen_env, "all_data")
with patch(
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock:
with (
patch(
"homeassistant.components.plugwise.coordinator.Smile", autospec=True
) as smile_mock,
patch(
"homeassistant.components.plugwise.config_flow.Smile",
new=smile_mock,
),
):
smile = smile_mock.return_value

smile.async_update.return_value = PlugwiseData(
Expand Down
Loading