Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d19d94d
add wattwaechter integration
smartcircuits Mar 9, 2026
1d78171
fix _attr_in_progress: use None instead of False
smartcircuits Mar 10, 2026
0d9e034
fix codeowner to smartcircuits
smartcircuits Mar 10, 2026
1eee386
slim down PR per review: single platform, no diagnostics/reauth/recon…
smartcircuits Apr 1, 2026
d78059b
fix CI: bronze tier, mypy, pylint, test imports
smartcircuits Apr 1, 2026
9f98d10
fix CI: add quality_scale to manifest, requirements, ruff fixes
smartcircuits Apr 1, 2026
56a5a77
address review feedback from joostlek
smartcircuits Apr 2, 2026
50146b3
fix CI: ruff PERF401, generated file ordering, CODEOWNERS
smartcircuits Apr 2, 2026
22eb28e
Merge branch 'dev' into add-wattwaechter-integration
smartcircuits Apr 2, 2026
ce041e5
Merge branch 'dev' into add-wattwaechter-integration
smartcircuits Apr 2, 2026
dcc3f57
Merge branch 'dev' into add-wattwaechter-integration
smartcircuits Apr 2, 2026
10bb44d
fix ruff import order, prettier JSON sorting, integrations.json format
smartcircuits Apr 2, 2026
5c6e4b7
fix ruff formatting, import sorting, add mypy.ini entry
smartcircuits Apr 2, 2026
8c3a8b4
fix circular import: use TYPE_CHECKING for WattwaechterConfigEntry
smartcircuits Apr 2, 2026
3888be2
fix ruff: remove parentheses from multi-except (python 3.14)
smartcircuits Apr 2, 2026
feb385d
fix integrations.json field order to match hassfest generation
smartcircuits Apr 2, 2026
0c6a291
address second round of review feedback
smartcircuits Apr 2, 2026
bec90b2
Merge remote-tracking branch 'upstream/dev' into add-wattwaechter-int…
smartcircuits Apr 2, 2026
e8b5151
add return type annotations to test fixtures
smartcircuits Apr 2, 2026
39397c2
address third round of review feedback
smartcircuits Apr 11, 2026
a2d2b8f
fix except clause syntax for python 3.12 compatibility
smartcircuits Apr 11, 2026
20aac42
raise ConfigEntryNotReady when no meter data available on first poll
smartcircuits Apr 11, 2026
3b0fcf8
remove PARALLEL_UPDATES constant
smartcircuits Apr 11, 2026
1b817b0
import WattwaechterConfigEntry from coordinator instead of package root
smartcircuits Apr 11, 2026
27e6a0d
restore PARALLEL_UPDATES and add None guard for mypy
smartcircuits Apr 11, 2026
1597e2f
raise UpdateFailed on WattwaechterNoDataError to preserve last known …
smartcircuits Apr 11, 2026
e21b584
store empty string instead of None for device_name in config entry data
smartcircuits Apr 11, 2026
172231c
remove unused _LOGGER from sensor module
smartcircuits Apr 11, 2026
e700542
remove leftover wifi diagnostic sensor icons
smartcircuits Apr 11, 2026
33923f3
use entity registry unique_id lookups instead of hardcoded entity IDs…
smartcircuits Apr 11, 2026
d4c5a36
use unknown error instead of cannot_connect when device_id is missing
smartcircuits Apr 11, 2026
1a90ac9
add test for auth error during coordinator first refresh
smartcircuits Apr 11, 2026
6c78fd5
add missing tests for full codecov coverage
smartcircuits Apr 11, 2026
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
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ homeassistant.components.waqi.*
homeassistant.components.water_heater.*
homeassistant.components.watts.*
homeassistant.components.watttime.*
homeassistant.components.wattwaechter.*
homeassistant.components.weather.*
homeassistant.components.web_rtc.*
homeassistant.components.webhook.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions homeassistant/components/wattwaechter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The WattWächter Plus integration."""
Copy link

Copilot AI Apr 2, 2026

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.

Copilot uses AI. Check for mistakes.

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]
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions "OTA firmware update entity with install progress tracking" but this integration only includes the SENSOR platform (line 20). There is no update platform or UpdateEntity implementation visible in the provided code. Either the update platform is missing from the implementation, or the PR description is inaccurate.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions "Diagnostics support with data redaction" but there is no diagnostics module (diagnostics.py) in the integration. The PLATFORMS list only includes Platform.SENSOR. Either the diagnostics support is missing from the implementation, or the PR description is inaccurate.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description claims "OTA firmware update entity with install progress tracking" but no update platform (update.py) is present in the integration. The PLATFORMS list only includes Platform.SENSOR, and there is no update entity implementation.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description claims an "OTA firmware update entity with install progress tracking" feature, but there is no update.py platform file in the component, and Platform.UPDATE is not included in the PLATFORMS list. Either the update entity should be implemented, or the PR description should be corrected to remove this claim.

Copilot uses AI. Check for mistakes.


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

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)
240 changes: 240 additions & 0 deletions homeassistant/components/wattwaechter/config_flow.py
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
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zeroconf_confirm token form schema + description_placeholders block is duplicated in the same method for the error retry case (later in async_step_zeroconf_confirm). Consider extracting a small helper (e.g., _show_zeroconf_token_form(errors: dict[str, str] | None = None)) so future changes (new placeholders, validation, optional token behavior) don’t require updating multiple copies.

Copilot uses AI. Check for mistakes.
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,
},
)

# 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,
},
)

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
14 changes: 14 additions & 0 deletions homeassistant/components/wattwaechter/const.py
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"
Loading
Loading