-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
Add WattWächter Plus integration #165238
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
base: dev
Are you sure you want to change the base?
Add WattWächter Plus integration #165238
Changes from all commits
d19d94d
1d78171
0d9e034
1eee386
d78059b
9f98d10
56a5a77
50146b3
22eb28e
ce041e5
dcc3f57
10bb44d
5c6e4b7
8c3a8b4
3888be2
feb385d
0c6a291
bec90b2
e8b5151
39397c2
a2d2b8f
20aac42
3b0fcf8
1b817b0
27e6a0d
1597e2f
e21b584
172231c
e700542
33923f3
d4c5a36
1a90ac9
6c78fd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| """The WattWächter Plus integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from aio_wattwaechter import Wattwaechter, WattwaechterConnectionError | ||
|
|
||
| from homeassistant.const import CONF_HOST, CONF_TOKEN, Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.exceptions import ConfigEntryNotReady | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
|
||
| from .const import DOMAIN | ||
| from .coordinator import WattwaechterConfigEntry, WattwaechterCoordinator | ||
|
|
||
| PLATFORMS = [Platform.SENSOR] | ||
|
||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, entry: WattwaechterConfigEntry | ||
| ) -> bool: | ||
| """Set up WattWächter Plus from a config entry.""" | ||
| host = entry.data[CONF_HOST] | ||
| token = entry.data.get(CONF_TOKEN) | ||
|
|
||
| session = async_get_clientsession(hass) | ||
| client = Wattwaechter(host, token=token, session=session) | ||
|
|
||
| try: | ||
| await client.alive() | ||
| except WattwaechterConnectionError as err: | ||
| raise ConfigEntryNotReady( | ||
| translation_domain=DOMAIN, | ||
| translation_key="cannot_connect", | ||
| translation_placeholders={"host": host}, | ||
| ) from err | ||
smartcircuits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| coordinator = WattwaechterCoordinator(hass, entry, client) | ||
| await coordinator.async_config_entry_first_refresh() | ||
|
|
||
| entry.runtime_data = coordinator | ||
|
|
||
| await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def async_unload_entry( | ||
| hass: HomeAssistant, entry: WattwaechterConfigEntry | ||
| ) -> bool: | ||
| """Unload a WattWächter Plus config entry.""" | ||
| return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| """Config flow for the WattWächter Plus integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from aio_wattwaechter import ( | ||
| Wattwaechter, | ||
| WattwaechterAuthenticationError, | ||
| WattwaechterConnectionError, | ||
| ) | ||
| import voluptuous as vol | ||
|
|
||
| from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
| from homeassistant.const import CONF_HOST, CONF_TOKEN | ||
| from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
| from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo | ||
|
|
||
| from .const import ( | ||
| CONF_DEVICE_ID, | ||
| CONF_DEVICE_NAME, | ||
| CONF_FW_VERSION, | ||
| CONF_MAC, | ||
| CONF_MODEL, | ||
| DOMAIN, | ||
| ) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class WattwaechterConfigFlow(ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow for WattWächter Plus.""" | ||
|
|
||
| VERSION = 1 | ||
|
|
||
| def __init__(self) -> None: | ||
| """Initialize the config flow.""" | ||
| self._host: str = "" | ||
| self._device_id: str = "" | ||
| self._model: str = "" | ||
| self._fw_version: str = "" | ||
| self._mac: str = "" | ||
|
|
||
| async def async_step_zeroconf( | ||
| self, discovery_info: ZeroconfServiceInfo | ||
| ) -> ConfigFlowResult: | ||
| """Handle zeroconf discovery.""" | ||
| _LOGGER.debug("Zeroconf discovery: %s", discovery_info) | ||
|
|
||
| self._host = str(discovery_info.host) | ||
|
|
||
| properties = discovery_info.properties | ||
| device_id_raw = properties.get("id", "") | ||
| self._model = properties.get("model", "WW-Plus") | ||
| self._fw_version = properties.get("ver", "") | ||
| self._mac = properties.get("mac", "") | ||
|
|
||
| self._device_id = device_id_raw.removeprefix("WWP-") | ||
|
|
||
| if not self._device_id: | ||
| return self.async_abort(reason="no_device_id") | ||
|
|
||
| await self.async_set_unique_id(self._device_id) | ||
| self._abort_if_unique_id_configured(updates={CONF_HOST: self._host}) | ||
|
|
||
| session = async_get_clientsession(self.hass) | ||
| client = Wattwaechter(self._host, session=session) | ||
| try: | ||
| await client.alive() | ||
| except WattwaechterConnectionError: | ||
| return self.async_abort(reason="cannot_connect") | ||
|
|
||
| self.context["title_placeholders"] = {"name": f"WattWächter {self._device_id}"} | ||
| return await self.async_step_zeroconf_confirm() | ||
|
|
||
| async def async_step_zeroconf_confirm( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> ConfigFlowResult: | ||
| """Confirm zeroconf discovery.""" | ||
| if user_input is None: | ||
| # First visit: check if device needs a token | ||
| session = async_get_clientsession(self.hass) | ||
| client = Wattwaechter(self._host, session=session) | ||
| try: | ||
| await client.system_info() | ||
| except WattwaechterAuthenticationError: | ||
| # Device requires a token, show form | ||
| return self.async_show_form( | ||
| step_id="zeroconf_confirm", | ||
| data_schema=vol.Schema( | ||
| { | ||
| vol.Required(CONF_TOKEN): str, | ||
| } | ||
| ), | ||
| description_placeholders={ | ||
| "model": self._model or "WattWächter Plus", | ||
| "firmware": self._fw_version or "unknown", | ||
| "host": self._host or "", | ||
| "device_id": self._device_id or "", | ||
| }, | ||
| ) | ||
|
Comment on lines
+89
to
+102
|
||
| except WattwaechterConnectionError: | ||
| return self.async_abort(reason="cannot_connect") | ||
| else: | ||
| # No token needed, create entry directly | ||
| device_name = await self._async_fetch_device_name() | ||
| title = device_name or f"WattWächter {self._device_id}" | ||
| return self.async_create_entry( | ||
| title=title, | ||
| data={ | ||
| CONF_HOST: self._host, | ||
| CONF_TOKEN: None, | ||
| CONF_DEVICE_ID: self._device_id, | ||
| CONF_DEVICE_NAME: device_name or "", | ||
| CONF_MODEL: self._model, | ||
| CONF_FW_VERSION: self._fw_version, | ||
| CONF_MAC: self._mac, | ||
| }, | ||
| ) | ||
smartcircuits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # User submitted token | ||
| errors: dict[str, str] = {} | ||
| token = user_input.get(CONF_TOKEN) | ||
|
|
||
| session = async_get_clientsession(self.hass) | ||
| client = Wattwaechter(self._host, token=token, session=session) | ||
| try: | ||
| await client.system_info() | ||
| except WattwaechterAuthenticationError: | ||
| errors["base"] = "invalid_auth" | ||
| except WattwaechterConnectionError: | ||
| errors["base"] = "cannot_connect" | ||
|
|
||
| if errors: | ||
| return self.async_show_form( | ||
| step_id="zeroconf_confirm", | ||
| data_schema=vol.Schema( | ||
| { | ||
| vol.Required(CONF_TOKEN): str, | ||
| } | ||
| ), | ||
| description_placeholders={ | ||
| "model": self._model or "WattWächter Plus", | ||
| "firmware": self._fw_version or "unknown", | ||
| "host": self._host or "", | ||
| "device_id": self._device_id or "", | ||
| }, | ||
| errors=errors, | ||
| ) | ||
|
|
||
| device_name = await self._async_fetch_device_name(token) | ||
| title = device_name or f"WattWächter {self._device_id}" | ||
| return self.async_create_entry( | ||
| title=title, | ||
| data={ | ||
| CONF_HOST: self._host, | ||
| CONF_TOKEN: token, | ||
| CONF_DEVICE_ID: self._device_id, | ||
| CONF_DEVICE_NAME: device_name or "", | ||
| CONF_MODEL: self._model, | ||
| CONF_FW_VERSION: self._fw_version, | ||
| CONF_MAC: self._mac, | ||
| }, | ||
| ) | ||
smartcircuits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| async def async_step_user( | ||
| self, user_input: dict[str, Any] | None = None | ||
| ) -> ConfigFlowResult: | ||
| """Handle manual configuration.""" | ||
| errors: dict[str, str] = {} | ||
|
|
||
| if user_input is not None: | ||
| host = user_input[CONF_HOST] | ||
| token = user_input.get(CONF_TOKEN) or None | ||
|
|
||
| session = async_get_clientsession(self.hass) | ||
| client = Wattwaechter(host, token=token, session=session) | ||
|
|
||
| try: | ||
| alive = await client.alive() | ||
| system_info = await client.system_info() | ||
| settings = await client.settings() | ||
| except WattwaechterAuthenticationError: | ||
| errors["base"] = "invalid_auth" | ||
| except WattwaechterConnectionError: | ||
| errors["base"] = "cannot_connect" | ||
|
|
||
| if not errors: | ||
| device_id = system_info.get_value("esp", "esp_id") or "" | ||
| fw_version = system_info.get_value("esp", "os_version") or alive.version | ||
| mac = system_info.get_value("wifi", "mac_address") or "" | ||
| device_name = settings.device_name or "" | ||
|
|
||
| if not device_id: | ||
| errors["base"] = "unknown" | ||
|
|
||
| if not errors: | ||
| await self.async_set_unique_id(device_id) | ||
| self._abort_if_unique_id_configured() | ||
|
|
||
| title = device_name or f"WattWächter {device_id}" | ||
|
|
||
| return self.async_create_entry( | ||
| title=title, | ||
| data={ | ||
| CONF_HOST: host, | ||
| CONF_TOKEN: token, | ||
| CONF_DEVICE_ID: device_id, | ||
| CONF_DEVICE_NAME: device_name or "", | ||
| CONF_MODEL: "WW-Plus", | ||
| CONF_FW_VERSION: fw_version, | ||
| CONF_MAC: mac, | ||
| }, | ||
| ) | ||
|
|
||
| return self.async_show_form( | ||
| step_id="user", | ||
| data_schema=vol.Schema( | ||
| { | ||
| vol.Required(CONF_HOST): str, | ||
| vol.Optional(CONF_TOKEN): str, | ||
| } | ||
| ), | ||
| errors=errors, | ||
| ) | ||
|
|
||
| async def _async_fetch_device_name(self, token: str | None = None) -> str | None: | ||
| """Try to fetch device_name from settings, return None on failure.""" | ||
| session = async_get_clientsession(self.hass) | ||
| client = Wattwaechter(self._host, token=token, session=session) | ||
| try: | ||
| settings = await client.settings() | ||
| except ( | ||
| WattwaechterConnectionError, | ||
| WattwaechterAuthenticationError, | ||
| ): | ||
| return None | ||
| else: | ||
| return settings.device_name | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """Constants for the WattWächter Plus integration.""" | ||
|
|
||
| DOMAIN = "wattwaechter" | ||
|
|
||
| DEFAULT_SCAN_INTERVAL = 120 | ||
|
|
||
| CONF_DEVICE_ID = "device_id" | ||
| CONF_DEVICE_NAME = "device_name" | ||
| CONF_MODEL = "model" | ||
| CONF_MAC = "mac" | ||
| CONF_FW_VERSION = "fw_version" | ||
|
|
||
| MANUFACTURER = "SmartCircuits GmbH" | ||
| DEVICE_NAME = "WattWächter Plus" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR description claims "MQTT conflict detection (prevents duplicate entities)" but there is no MQTT-related code in the integration. This feature is not implemented in the provided code.