Skip to content
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

Versions from 0.40 and up

## Ongoing
## v0.55.5

- Rework quality improvements from Core Quality Scale

## v0.55.4

- Link to plugwise [v1.6.4](https://github.com/plugwise/python-plugwise/releases/tag/v1.6.4)
- Internal: Adjustments in the CI process

## v0.55.3
Expand Down
4 changes: 2 additions & 2 deletions custom_components/plugwise/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
from .coordinator import PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity

PARALLEL_UPDATES = 0 # Upstream
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0


@dataclass(frozen=True)
Expand All @@ -54,7 +55,6 @@ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription):
PLUGWISE_BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = (
PlugwiseBinarySensorEntityDescription(
key=BATTERY_STATE,
translation_key=BATTERY_STATE,
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
),
Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

PARALLEL_UPDATES = 0 # Upstream
PARALLEL_UPDATES = 0


async def async_setup_entry(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

PARALLEL_UPDATES = 0 # Upstream
PARALLEL_UPDATES = 0


async def async_setup_entry(
Expand Down
138 changes: 96 additions & 42 deletions custom_components/plugwise/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from copy import deepcopy
import logging
from typing import Any, Self

from plugwise import Smile
Expand Down Expand Up @@ -69,18 +70,25 @@

type PlugwiseConfigEntry = ConfigEntry[PlugwiseDataUpdateCoordinator]

_LOGGER = logging.getLogger(__name__)

# Upstream basically the whole file (excluding the pw-beta options)

SMILE_RECONF_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)


def base_schema(
cf_input: ZeroconfServiceInfo | dict[str, Any] | None,
) -> vol.Schema:
def smile_user_schema(cf_input: ZeroconfServiceInfo | dict[str, Any] | None) -> vol.Schema:
"""Generate base schema for gateways."""
if not cf_input: # no discovery- or user-input available
return vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PASSWORD): str,
# Port under investigation for removal (hence not added in #132878)
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
vol.Required(CONF_USERNAME, default=SMILE): vol.In(
{SMILE: FLOW_SMILE, STRETCH: FLOW_STRETCH}
Expand All @@ -106,7 +114,7 @@ def base_schema(
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 the schema with values provided by the user.
"""
websession = async_get_clientsession(hass, verify_ssl=False)
api = Smile(
Expand All @@ -120,6 +128,32 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile:
return api


async def verify_connection(
hass: HomeAssistant, user_input: dict[str, Any]
) -> tuple[Smile | None, dict[str, str]]:
"""Verify and return the gateway connection or an error."""
errors: dict[str, str] = {}

try:
return (await validate_input(hass, user_input), errors)
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
_LOGGER.exception(
"Unknown exception while verifying connection with your Plugwise Smile"
)
errors[CONF_BASE] = "unknown"
return (None, errors)


class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Plugwise Smile."""

Expand Down Expand Up @@ -197,51 +231,71 @@ def is_matching(self, other_flow: Self) -> bool:

return False


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:
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=base_schema(self.discovery_info),
errors=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:
return self.async_show_form(
step_id=SOURCE_USER,
data_schema=base_schema(user_input),
errors=errors,
)

await self.async_set_unique_id(
api.smile_hostname or api.gateway_id, raise_on_progress=False
if user_input is not None:
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

api, errors = await verify_connection(self.hass, user_input)
if api:
await self.async_set_unique_id(
api.smile_hostname or api.gateway_id, raise_on_progress=False
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=api.smile_name, data=user_input)

return self.async_show_form(
step_id=SOURCE_USER,
data_schema=smile_user_schema(self.discovery_info),
errors=errors,
)
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."""
errors: dict[str, str] = {}

reconfigure_entry = self._get_reconfigure_entry()

if user_input:
# Redefine ingest existing username and password
full_input = {
CONF_HOST: user_input.get(CONF_HOST),
CONF_PORT: reconfigure_entry.data.get(CONF_PORT),
CONF_USERNAME: reconfigure_entry.data.get(CONF_USERNAME),
CONF_PASSWORD: reconfigure_entry.data.get(CONF_PASSWORD),
}

api, errors = await verify_connection(self.hass, full_input)
if 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,
)

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=errors,
)


@staticmethod
@callback
Expand Down
24 changes: 19 additions & 5 deletions custom_components/plugwise/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,31 @@ async def _async_update_data(self) -> PlugwiseData:
await self._connect()
data = await self.api.async_update()
except ConnectionFailedError as err:
raise UpdateFailed("Failed to connect") from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="failed_to_connect",
) from err
except InvalidAuthentication as err:
raise ConfigEntryError("Authentication failed") from err
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="authentication_failed",
) from err
except (InvalidXMLError, ResponseError) as err:
# pwbeta TODO; we had {err} in the text, but not upstream, do we want this?
raise UpdateFailed(
f"Invalid XML data or error from Plugwise device: {err}"
translation_domain=DOMAIN,
translation_key="invalid_xml_data",
) from err
except PlugwiseError as err:
raise UpdateFailed("Data incomplete or missing") from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="data_incomplete_or_missing",
) from err
except UnsupportedDeviceError as err:
raise ConfigEntryError("Device with unsupported firmware") from err
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="unsupported_firmware",
) from err
else:
LOGGER.debug(f"{self.api.smile_name} data: %s", data)
await self.async_add_remove_devices(data, self.config_entry)
Expand Down
5 changes: 0 additions & 5 deletions custom_components/plugwise/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,3 @@ def available(self) -> bool:
def device(self) -> GwEntityData:
"""Return data for this device."""
return self.coordinator.data.devices[self._dev_id]

async def async_added_to_hass(self) -> None:
"""Subscribe to updates."""
self._handle_coordinator_update()
await super().async_added_to_hass()
2 changes: 1 addition & 1 deletion custom_components/plugwise/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["plugwise"],
"requirements": ["plugwise==1.6.4"],
"version": "0.55.4",
"version": "0.55.5",
"zeroconf": ["_plugwise._tcp.local."]
}
2 changes: 1 addition & 1 deletion custom_components/plugwise/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

PARALLEL_UPDATES = 0 # Upstream
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

PARALLEL_UPDATES = 0 # Upstream
PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
Expand Down
1 change: 1 addition & 0 deletions custom_components/plugwise/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
from .coordinator import PlugwiseDataUpdateCoordinator
from .entity import PlugwiseEntity

# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0


Expand Down
34 changes: 30 additions & 4 deletions 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,14 +49,13 @@
},
"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": {
"binary_sensor": {
"low_battery": {
"name": "Battery state"
},
"compressor_state": {
"name": "Compressor state"
},
Expand Down Expand Up @@ -296,6 +302,26 @@
}
}
},
"exceptions": {
"authentication_failed": {
"message": "Invalid authentication"
},
"data_incomplete_or_missing": {
"message": "Data incomplete or missing."
},
"error_communicating_with_api": {
"message": "Error communicating with API: {error}."
},
"failed_to_connect": {
"message": "Failed to connect"
},
"invalid_xml_data": {
"message": "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
},
"unsupported_firmware": {
"message": "Device with unsupported firmware"
}
},
"services": {
"delete_notification": {
"name": "Delete Plugwise notification",
Expand Down
2 changes: 1 addition & 1 deletion custom_components/plugwise/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .entity import PlugwiseEntity
from .util import plugwise_command

PARALLEL_UPDATES = 0 # Upstream
PARALLEL_UPDATES = 0


@dataclass(frozen=True)
Expand Down
Loading
Loading