Skip to content

Add Wibeee energy monitoring integration#168419

Open
fquinto wants to merge 83 commits intohome-assistant:devfrom
fquinto:dev
Open

Add Wibeee energy monitoring integration#168419
fquinto wants to merge 83 commits intohome-assistant:devfrom
fquinto:dev

Conversation

@fquinto
Copy link
Copy Markdown

@fquinto fquinto commented Apr 17, 2026

Proposed change

Add a new integration for wibeee energy monitoring devices.

The Wibeee integration allows Home Assistant to connect to Wibeee energy meters over the local network and retrieve real-time electrical measurements, including power, voltage, current, and energy consumption.

Key features:

  • Local polling via HTTP (status.xml)
  • Optional local push mode for faster updates
  • Support for multiple device models (single-phase and three-phase)
  • Automatic device configuration for push mode
  • Sensor entities for electrical measurements (power, voltage, current, energy, etc.)
  • Button entities for device control (reboot, reset energy)
  • Full config flow support
  • Diagnostics support

The integration is fully local and does not require any cloud connectivity.


Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

fquinto added 3 commits April 17, 2026 11:48
- Add Wibeee energy monitor integration (new integration)
- Supports local push and polling modes for device data retrieval
- Platforms: sensor, button, diagnostics
- Quality scale: all Bronze/Silver/Gold/Platinum items completed
- Add comprehensive test suite:
  - conftest.py with fixtures and mocks
  - test_init.py for integration setup
  - test_config_flow.py for config flow
  - test_sensor.py for sensor platform
  - test_button.py for button platform
- Full integration with local push and polling modes
- Platforms: sensor, button, diagnostics
- Config flow with DHCP discovery support
- Push receiver for real-time updates
- Icon and translation support
Copy link
Copy Markdown
Contributor

@home-assistant home-assistant Bot left a comment

Choose a reason for hiding this comment

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

When adding new integrations, limit included platforms to a single platform. Please reduce this PR to a single platform. See the review process for more details.

@home-assistant home-assistant Bot marked this pull request as draft April 17, 2026 10:11
@home-assistant
Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Comment thread homeassistant/components/wibeee/api.py Outdated
Copy link
Copy Markdown
Member

@erwindouna erwindouna Apr 17, 2026

Choose a reason for hiding this comment

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

This should be in its own library and cannot be part of the HA integration. Please split it off and create it as a package on pypi.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the feedback!

I will extract the API client into a standalone library and update the integration to use it as an external dependency.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new wibeee integration to Home Assistant to support Wibeee energy monitors via local polling and optional local HTTP push, including config flow, entities, and diagnostics.

Changes:

  • Introduces the core integration modules (API client, coordinator, config flow, sensor + button platforms, push receiver, diagnostics).
  • Adds translations/icons and quality scale metadata for the integration.
  • Adds an initial test suite and fixtures for config flow and entity creation.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
homeassistant/components/wibeee/__init__.py Sets up config entries, selects polling vs push mode, wires coordinator/platforms.
homeassistant/components/wibeee/api.py Implements local HTTP client for status/device info and device actions/config.
homeassistant/components/wibeee/button.py Adds reboot/reset-energy button entities backed by the API.
homeassistant/components/wibeee/config_flow.py Implements user/DHCP config flow plus options flow (mode + interval + auto-configure).
homeassistant/components/wibeee/const.py Defines integration constants, push param mapping, and sensor entity descriptions.
homeassistant/components/wibeee/coordinator.py Coordinator for polling updates and push-injected updates.
homeassistant/components/wibeee/diagnostics.py Provides diagnostics payload with redaction support.
homeassistant/components/wibeee/push_receiver.py Registers unauthenticated HTTP endpoints to receive device push updates and route them by MAC.
homeassistant/components/wibeee/manifest.json Declares integration metadata, requirements, dependencies, DHCP matching.
homeassistant/components/wibeee/strings.json UI strings for config/options/abort/errors and entity names.
homeassistant/components/wibeee/icons.json Icon translations for entities.
homeassistant/components/wibeee/quality_scale.yaml Declares quality scale status for the new integration.
tests/components/wibeee/conftest.py Adds fixtures and API mocks for integration/config flow tests.
tests/components/wibeee/test_config_flow.py Adds config flow and options flow tests.
tests/components/wibeee/test_init.py Adds basic integration setup tests.
tests/components/wibeee/test_sensor.py Adds basic sensor platform tests.
tests/components/wibeee/test_button.py Adds basic button platform tests.
tests/components/wibeee/__init__.py Declares the tests package.

Comment thread tests/components/wibeee/conftest.py Outdated
Comment thread tests/components/wibeee/test_init.py Outdated
Comment thread tests/components/wibeee/test_button.py Outdated
Comment thread homeassistant/components/wibeee/const.py
Comment thread homeassistant/components/wibeee/const.py
Comment thread tests/components/wibeee/test_init.py Outdated
Comment thread tests/components/wibeee/test_config_flow.py
Comment thread homeassistant/components/wibeee/manifest.json Outdated
Comment thread homeassistant/components/wibeee/__init__.py
Comment thread homeassistant/components/wibeee/config_flow.py Outdated
fquinto added 2 commits April 17, 2026 12:25
- Remove 'homeassistant', 'issue_tracker', 'version' (not valid in core)
- Add 'network' to dependencies for network functionality
Copilot AI review requested due to automatic review settings April 17, 2026 10:27
- Fix DeviceInfo import: use device_registry instead of entity
- Fix config_flow options schema with proper vol.Optional usage
- Remove runtime_data = None in unload (not needed in HA core)
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.

Comment thread homeassistant/components/wibeee/sensor.py Outdated
Comment thread homeassistant/components/wibeee/__init__.py Outdated
Comment thread homeassistant/components/wibeee/manifest.json Outdated
Comment thread tests/components/wibeee/conftest.py
Comment thread homeassistant/components/wibeee/__init__.py
Comment thread homeassistant/components/wibeee/__init__.py Outdated
Comment thread homeassistant/components/wibeee/sensor.py Outdated
fquinto and others added 2 commits April 17, 2026 12:59
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 17, 2026 11:09
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
fquinto and others added 3 commits April 17, 2026 13:09
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated 5 comments.

Comment on lines +45 to +51
"device": {
"wibeee_id": device_info.wibeee_id,
"mac_addr": "**REDACTED**",
"model": device_info.model,
"firmware_version": device_info.firmware_version,
"ip_addr": "**REDACTED**",
},
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Diagnostics redaction is using the literal string "REDACTED" instead of Home Assistant's standard REDACTED sentinel value. This will break expectations in tests (and consistency across integrations). Use homeassistant.components.diagnostics.REDACTED (or run these fields through async_redact_data) so the output matches the framework constant.

Copilot uses AI. Check for mistakes.
Comment on lines +175 to +204
@property
def native_value(self) -> float | None:
"""Return the sensor value."""
data = self.coordinator.data
if data is None:
return None
phase_data = data.get(self._phase_key)
if phase_data is None:
return None
value = phase_data.get(self.entity_description.key)
if value is None:
return None
try:
return float(value)
except ValueError, TypeError:
return None

@property
def available(self) -> bool:
"""Return True if the coordinator has data for this sensor.

Extends CoordinatorEntity.available (which checks coordinator
connectivity) with phase/key-level granularity.
"""
if not super().available:
return False
phase_data = (self.coordinator.data or {}).get(self._phase_key)
if phase_data is None:
return False
return self.entity_description.key in phase_data
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

If the key exists but the value is non-numeric, native_value returns None but available still returns True, which yields an unknown state rather than unavailable. Your tests expect invalid numeric values to become STATE_UNAVAILABLE. To align behavior, make invalid/unparseable values mark the entity unavailable (e.g., validate/coerce values centrally in the coordinator when setting updated data, or have available also treat non-castable values as unavailable).

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +78
# Discover phases from initial data (hardware-dependent).
# Single-phase: fase1 + fase4. Three-phase: fase1-3 + fase4.
data = coordinator.data
if data is None:
_LOGGER.warning(
"No data available for Wibeee %s (%s); no sensors created",
device_info.mac_addr_short,
device_info.ip_addr,
)
return

discovered_phases = list(data.keys())
if not discovered_phases:
_LOGGER.warning(
"No phases found for Wibeee %s (%s)",
device_info.mac_addr_short,
device_info.ip_addr,
)
return
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

Phase discovery currently uses list(data.keys()) without filtering. If the API payload ever includes non-phase top-level keys, that would create sensors (and devices) for invalid phase_key values. Filter to known phase keys (e.g., fase1-fase4) before creating entities.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +19
The PushReceiver is a singleton stored in ``hass.data[DOMAIN]``. Each
config entry registers its MAC address so incoming push data is routed
to the correct sensor entities.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The module docstring says the singleton is stored in hass.data[DOMAIN], but async_setup_push_receiver stores it under the DATA_PUSH_RECEIVER key (i.e., hass.data[\"wibeee_push_receiver\"]). Update the docstring to match the actual storage location to avoid misleading future maintainers.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +323
# If local push + auto-configure, configure the device now
if mode == MODE_LOCAL_PUSH and auto_configure:
try:
local_ip = await _get_local_ip(self.hass)
if not _is_routable_ip(local_ip):
_LOGGER.warning(
"Detected non-routable local IP %s for auto-configuration. "
"Please configure push manually via the device web interface",
local_ip,
)
errors["base"] = "auto_configure_failed"
else:
ha_port = _get_ha_port(self.hass)
session = async_get_clientsession(self.hass)
api = WibeeeAPI(
session,
self._user_data[CONF_HOST],
timeout=timedelta(seconds=15),
)
success = await api.async_configure_push_server(
local_ip, ha_port
)
if not success:
errors["base"] = "auto_configure_failed"
else:
_LOGGER.debug(
"Auto-configured WiBeee to push to %s:%d",
local_ip,
ha_port,
)
except TimeoutError, aiohttp.ClientError, OSError:
_LOGGER.debug(
"Failed to auto-configure WiBeee at %s",
self._user_data[CONF_HOST],
exc_info=True,
)
errors["base"] = "auto_configure_failed"
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The push auto-configuration logic is duplicated in both the main config flow (async_step_mode) and the options flow (async_step_init). Extracting this into a shared helper (returning success/failure and handling routable-IP checks) would reduce drift risk and make future changes (timeouts, logging, error mapping) easier.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings April 27, 2026 17:34
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated 4 comments.

Comment on lines +67 to +73
f"Wibeee {device.mac_addr_short}",
device.mac_addr_formatted,
{
CONF_HOST: data[CONF_HOST],
CONF_MAC_ADDRESS: device.mac_addr_formatted,
CONF_WIBEEE_ID: device.wibeee_id,
},
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Normalize the MAC-based unique_id to a single canonical format so DHCP discovery and manual setup don't create duplicate entries for the same device.

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +45
DEFAULT_HA_PORT = 8123

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Reuse the existing DEFAULT_HA_PORT constant from const.py instead of redefining it here to avoid the two values drifting apart over time.

Copilot uses AI. Check for mistakes.
raise ConfigEntryNotReady(
f"Could not fetch initial sensor data from Wibeee at {host}"
)

Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Validate that the initial sensor payload fetched in push mode is a dict before storing it in the coordinator to avoid runtime errors in sensor entities when the library returns an unexpected type.

Suggested change
if not isinstance(initial_data, dict):
raise ConfigEntryNotReady(
f"Invalid initial sensor data received from Wibeee at {host}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +97
# Build entities: discovered phases x ALL sensor types (deterministic).
# Process fase4 (Total) first to ensure the parent device exists
# before child phase devices that reference it via via_device.
sorted_phases = sorted(
discovered_phases,
key=lambda p: (0 if p == "fase4" else 1, p),
)
entities: list[WibeeeSensor] = [
WibeeeSensor(
coordinator=coordinator,
device_info=device_info,
phase_key=phase_key,
description=description,
)
for phase_key in sorted_phases
for description in SENSOR_TYPES.values()
]
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

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

Only create sensor entities for keys actually present (or clearly supported) for the discovered phases to avoid registering dozens of entities that will permanently stay unavailable.

Suggested change
# Build entities: discovered phases x ALL sensor types (deterministic).
# Process fase4 (Total) first to ensure the parent device exists
# before child phase devices that reference it via via_device.
sorted_phases = sorted(
discovered_phases,
key=lambda p: (0 if p == "fase4" else 1, p),
)
entities: list[WibeeeSensor] = [
WibeeeSensor(
coordinator=coordinator,
device_info=device_info,
phase_key=phase_key,
description=description,
)
for phase_key in sorted_phases
for description in SENSOR_TYPES.values()
]
# Build entities only for sensor keys present in each discovered phase.
# Process fase4 (Total) first to ensure the parent device exists
# before child phase devices that reference it via via_device.
sorted_phases = sorted(
discovered_phases,
key=lambda p: (0 if p == "fase4" else 1, p),
)
entities: list[WibeeeSensor] = []
for phase_key in sorted_phases:
phase_data = data.get(phase_key)
if not isinstance(phase_data, dict):
continue
for description in SENSOR_TYPES.values():
if description.key not in phase_data:
continue
entities.append(
WibeeeSensor(
coordinator=coordinator,
device_info=device_info,
phase_key=phase_key,
description=description,
)
)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated 7 comments.

Comment on lines +208 to +212
phase_data = (self.coordinator.data or {}).get(self._phase_key)
if phase_data is None:
return False
value = phase_data.get(self.entity_description.key)
if value is None:
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Validate phase_data is a dict in available before reading sensor keys to avoid attribute errors when coordinator data is malformed.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +15
For each discovered phase, **all** ``SENSOR_TYPES`` are created
deterministically. Sensors whose keys are not present in the data
report ``available=False`` and ``native_value=None``.
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Align the docstring with the actual setup logic, since the code only creates entities for sensor keys present in the initial data.

Suggested change
For each discovered phase, **all** ``SENSOR_TYPES`` are created
deterministically. Sensors whose keys are not present in the data
report ``available=False`` and ``native_value=None``.
For each discovered phase, entities are created only for
``SENSOR_TYPES`` whose keys are present in the initial phase data.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +69
return (
f"Wibeee {device.mac_addr_short}",
device.mac_addr_formatted,
{
CONF_HOST: data[CONF_HOST],
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Normalize the config-entry unique_id (e.g., lowercase and remove colons) so DHCP discovery and manual setup use the same format and don't create duplicate entries.

Copilot uses AI. Check for mistakes.
return WibeeeOptionsFlowHandler()

async def async_step_reconfigure(
self, user_input: dict | None = None
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Add concrete type parameters for user_input in async_step_reconfigure to satisfy the repository mypy setting disallow_any_generics = true.

Suggested change
self, user_input: dict | None = None
self, user_input: dict[str, Any] | None = None

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +122
import socket # noqa: PLC0415

try:
resolved_ip = await hass.async_add_executor_job(socket.gethostbyname, host)
except OSError:
resolved_ip = host
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Ensure resolved_ip is always an actual IP (or disable IP validation) because storing the hostname here will cause all push requests to be rejected by the receiver's strict remote_ip == expected_ip check.

Suggested change
import socket # noqa: PLC0415
try:
resolved_ip = await hass.async_add_executor_job(socket.gethostbyname, host)
except OSError:
resolved_ip = host
import ipaddress # noqa: PLC0415
import socket # noqa: PLC0415
try:
resolved_ip = str(ipaddress.ip_address(host))
except ValueError:
try:
resolved_ip = await hass.async_add_executor_job(socket.gethostbyname, host)
resolved_ip = str(ipaddress.ip_address(resolved_ip))
except (OSError, ValueError) as err:
raise ConfigEntryNotReady(
f"Could not resolve Wibeee host {host} to an IP address for push mode"
) from err

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +192
phase_data = data.get(self._phase_key)
if phase_data is None:
return None
value = phase_data.get(self.entity_description.key)
if value is None:
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Guard against non-dict phase payloads before calling .get() so the entity doesn't crash if the coordinator receives malformed data.

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +212
if not super().available:
return False
phase_data = (self.coordinator.data or {}).get(self._phase_key)
if phase_data is None:
return False
value = phase_data.get(self.entity_description.key)
if value is None:
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Validate phase_data is a dict in available before reading sensor keys to avoid attribute errors when coordinator data is malformed.

Copilot uses AI. Check for mistakes.
fquinto added 2 commits April 30, 2026 16:10
…slations

- Rename mock_get_source_ip fixture to mock_wibeee_local_ip to avoid
  shadowing the global session-scoped fixture in tests/conftest.py.
  The shadow caused http+network setup to attempt real socket use.
- Patch WibeeeAPI in all import locations (__init__, config_flow,
  coordinator) so mocked instances reach validate_input and setup_entry.
- Make mock_wibeee_api autouse so all wibeee tests get the mock.
- Add missing options.error.auto_configure_failed translation key
  required by the options flow error handling.

All 35 wibeee tests pass with Python 3.14.2 and ruff/mypy/hassfest
report no issues for the wibeee integration.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated 2 comments.

Comment on lines +32 to +33
PLATFORMS = [Platform.SENSOR]

Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

Align the implementation with the PR description by either adding the button platform (and including Platform.BUTTON in PLATFORMS) or updating the PR description/translations/icons to match the current sensor-only integration.

Copilot uses AI. Check for mistakes.
Comment thread homeassistant/components/wibeee/config_flow.py
fquinto added 2 commits May 1, 2026 14:42
…orm and add reconfigure tests

- Remove button references from icons.json and strings.json since
  PLATFORMS only contains Platform.SENSOR
- Add reconfigure_successful translation key
- Add three config flow tests covering the reconfigure step:
  success, wrong_device abort, and no_device_info error
Add coverage for previously-untested paths flagged by codecov/patch:
- __init__.py (82% -> 100%): connection errors raising ConfigEntryNotReady,
  device_info=None fallback, push mode initial fetch errors, hostname
  resolution success/failure, unload entry, options reload
- config_flow.py (74% -> 99%): _is_routable_ip parametrized cases,
  _get_local_ip_sync (success + OSError fallback), _get_local_ip
  (network/get_url/executor branches), _get_ha_port branches,
  _async_configure_device (success/timeout/non-routable IP),
  DHCP already-configured/not-Wibeee/connection-error,
  user step already_configured + unknown error,
  reconfigure step unknown error
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated 5 comments.

Comment on lines +93 to +95
if isinstance(data.get(phase_key), dict)
for sensor_key, description in SENSOR_TYPES.items()
if sensor_key in data[phase_key]
"error": {
"auto_configure_failed": "Failed to auto-configure the device for Local Push. You can configure it manually via the device web interface.",
"no_device_info": "Could not connect to the WiBeee device. Verify the IP address and that the device is powered on.",
"unknown": "Unknown error occurred."
Comment on lines +100 to +103
try:
initial_data = await api.async_fetch_sensors_data(retries=3)
except (TimeoutError, aiohttp.ClientError) as err:
raise ConfigEntryNotReady(f"Error connecting to Wibeee at {host}") from err
Comment on lines +105 to +110
if not initial_data:
raise ConfigEntryNotReady(
f"Could not fetch initial sensor data from Wibeee at {host}"
)

coordinator.async_set_updated_data(initial_data)
Comment on lines +92 to +97
coordinator = WibeeeCoordinator(
hass,
api,
config_entry=entry,
name=f"Wibeee {device_info.mac_addr_short}",
update_interval=None,
fquinto added 2 commits May 2, 2026 10:51
- Filter sensors in push mode to keys parse_push_data() can refresh
  (PUSH_REFRESHABLE_SENSOR_KEYS), so polling-only metrics like THD,
  angle, and capacitive-reactive variants do not become unavailable
  after the first push update.
- Catch XMLParseError in addition to TimeoutError/ClientError during
  the push-mode bootstrap fetch so a malformed status.xml puts the
  entry into retry instead of crashing setup.
- Reject non-dict bootstrap responses before seeding the coordinator,
  so a stray string/list cannot make setup finish with zero entities.
- Add a push staleness watchdog (PUSH_STALE_AFTER = 5 min). The
  coordinator marks itself failed if no push arrives within the window;
  every async_push_update resets it. Cancelled on shutdown.
- Fix grammar in 'unknown' error message ('An unknown error occurred').
Cover the remaining branches reported missing by codecov/patch:

- config_flow.py: hostname (non-IP) fallback in _get_local_ip's get_url
  branch when ipaddress.ip_address() raises ValueError.
- coordinator.py: early return in _reschedule_staleness_check when
  stale_after is None (polling-mode coordinators).
- diagnostics.py: _redact_coordinator_data with None input.
- push_receiver.py: parse_push_data short-param skip, dispatch with
  no recognized sensor params, and the three view classes
  (WibeeeReceiverAvgView, WibeeeReceiverView, WibeeeReceiverLeapView).
- sensor.py: async_setup_entry early returns (no data, no phases) and
  WibeeeSensor.native_value defensive branches (non-dict data,
  non-dict phase data, missing key, non-numeric value).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

The previous CI run failed on
tests/components/zwave_js/test_device_trigger.py::test_no_controller_triggers
with an SQLAlchemy SingletonThreadPool reset error \u2014 unrelated to the
wibeee integration. All wibeee tests passed (92/92, 100% patch coverage).
This empty commit retriggers CI to clear the flaky failure.
@fquinto
Copy link
Copy Markdown
Author

fquinto commented May 3, 2026

Thank you for the thorough review and recommendations! The PR has been updated to align with Home Assistant core standards. A summary of the changes:

Style & API surface

  • Simplified the docstring in __init__.py and removed the unnecessary re-export of DOMAIN.
  • Reduced the integration to a single platform (sensor) to streamline the initial review, as suggested.
  • Lowered quality_scale to bronze to match the scope of the initial submission.

Linting & translations

  • Fixed user-visible logger messages (consistent punctuation, no trailing periods on log records, proper sentence form on user-facing strings).
  • Reordered translation JSON files alphabetically and regenerated translations/en.json from strings.json via python -m script.translations develop --all.

Security & robustness

  • Source IP validation for Local Push: incoming push requests from registered devices are matched against the IP they were registered with, reducing the risk of spoofed pushes from arbitrary sources on the LAN.
  • Push staleness watchdog (PUSH_STALE_AFTER = 5 min): if no push arrives within the window, the coordinator is marked failed and entities go unavailable instead of reporting stale last-known values.
  • Bootstrap hardening: the initial fetch now catches XMLParseError and rejects non-dict / empty payloads, raising ConfigEntryNotReady so HA retries cleanly.
  • Push-mode sensor filter: only sensor keys refreshable via push (PUSH_REFRESHABLE_SENSOR_KEYS) are created in push mode, preventing polling-only metrics (THD, angle, capacitive-reactive, etc.) from becoming permanently unavailable.
  • _get_local_ip only ever returns IP literals (the get_url fallback skips hostname results so we never register a non-IP for source-IP validation).

Typing & syntax

  • Improved type hints in diagnostics.py and across the package.
  • Verified full compatibility with Python 3.14 syntax (PEP 758 — except A, B: form is intentional and accepted by Python 3.14 / ruff).

Tests

  • Test suite expanded: 92 tests, 100% line coverage across all wibeee modules (__init__, config_flow, coordinator, diagnostics, push_receiver, sensor, const).
  • Added reconfigure-flow tests (including the new reconfigure_successful translation) and tests for the staleness watchdog, push-mode sensor filtering, source-IP validation, and the three push HTTP views.

CI note

The previous run had a single failure in an unrelated flaky test (tests/components/zwave_js/test_device_trigger.py::test_no_controller_triggers, an SQLAlchemy SingletonThreadPool reset error). All wibeee tests pass locally and on CI; an empty commit was pushed to retrigger the workflow.

Ready for another look. Thanks!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

if data is None:
raise UpdateFailed(f"No data received from Wibeee at {self.api.host}")

if not isinstance(data, dict):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In which scenario's can this happen? :)

"""Coordinator for Wibeee sensor data.

In polling mode, ``_async_update_data`` fetches from the device API.
In push mode, ``update_interval`` is None and data is injected
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure if this is feasible and/or implemented correctly. When a DUC is set to poll, you should indicate this Entity.should_poll. I'm leaning against to advise polling here, since that would potentially reduce the push_receiver?


_LOGGER = logging.getLogger(__name__)

# Type alias: phase_key -> sensor_key -> value
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Type alias: phase_key -> sensor_key -> value

Comment on lines +50 to +54
"""Validate the user input allows us to connect.

Returns:
A tuple of (title, unique_id, data).
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
"""Validate the user input allows us to connect.
Returns:
A tuple of (title, unique_id, data).
"""
"""Validate the user input allows us to connect."""

Please try to compact the docstrings, and try to keep them onliners, if you can. :)

raise NoDeviceInfo(f"Cannot connect: {exc}") from exc

if device is None:
raise NoDeviceInfo("No device info received")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Won't the library throw an exception for this?

async def _get_local_ip(hass: HomeAssistant) -> str:
"""Determine the local IP of the Home Assistant instance."""
try:
from homeassistant.components.network import ( # noqa: PLC0415
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please don't ignore these warnings. Late-imports aren't implemented the correct way here. Most common when using late-imports is to break circular imports, optional dependencies behind a feature flag, or really really heavy library which are rarely used. I don't think this is the case here. :)

pass

try:
from homeassistant.helpers.network import get_url # noqa: PLC0415
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same feedback on the late-imports.

def _get_ha_port(hass: HomeAssistant) -> int:
"""Get the port Home Assistant's HTTP server is listening on."""
try:
from homeassistant.helpers.network import get_url # noqa: PLC0415
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Late-imports feedback.

- coordinator.py: drop the dead non-dict UpdateFailed branch (the
  pywibeee API is typed to return dict | None, never another type).
- coordinator.py: drop the pointless type-alias comment and compact
  the docstrings; expand the class docstring to clarify the
  mutually-exclusive polling vs push modes.
- config_flow.py: move late imports (async_get_source_ip, get_url) to
  the module top level. The 'network' integration is already declared
  as a dependency in manifest.json, so no circular-import risk.
- config_flow.py: compact validate_input docstring to a one-liner;
  add a comment explaining why the 'device is None' check is required
  (the library returns None instead of raising when MAC discovery
  fails, see pywibeee/client.py:async_fetch_device_info).
- config_flow.py: simplify _get_local_ip / _get_ha_port control flow.
- Update tests to patch the module-level import names; remove the
  test for the dropped non-dict UpdateFailed branch; add a small test
  for _get_ha_port when get_url returns no port.

Coverage stays at 100% (92 tests passing).
@fquinto
Copy link
Copy Markdown
Author

fquinto commented May 4, 2026

Thanks for the careful review @erwindouna! All points addressed in commit e381e63:

Coordinator

  • not isinstance(data, dict) branch removed. You're right — pywibeee.WibeeeAPI.async_fetch_sensors_data is typed dict[str, dict[str, str]] | None, so the type check was dead defensive code. The data is None check stays because the library does return None (e.g. when status.xml is empty).
  • Type-alias comment removed.
  • Polling vs push docstring clarified. The two modes are mutually exclusive and chosen at setup time — there is no mixed mode and the entity never sets should_poll. In push mode, update_interval is None (the DUC does not poll), the coordinator acts as a passive bus, and data is injected via async_set_updated_data() from the HTTP receiver. A staleness watchdog marks entities unavailable if no push is received within PUSH_STALE_AFTER (5 minutes). I expanded the class docstring to make this explicit.
  • Compacted the constructor / async_push_update docstrings.

Config flow

  • validate_input docstring reduced to a one-liner.
  • device is None retained with an inline comment. The pywibeee library returns None (without raising) when device info is incomplete — concretely, when the MAC address cannot be determined from values.xml (see pywibeee/client.py::async_fetch_device_info). Treating that as NoDeviceInfo ensures a consistent error flow in the config flow. The library currently returns None when device info is incomplete (e.g. missing MAC address), so this is handled here as NoDeviceInfo.
  • Late imports removed. Moved async_get_source_ip and get_url to module-level imports and dropped the # noqa: PLC0415. The network integration is already declared in manifest.json ("dependencies": ["http", "network"]), so there is no circular-import risk.
  • Simplified the control flow of _get_local_ip and _get_ha_port accordingly.

Tests

  • Updated patch targets to the new module-level import names.
  • Removed the test for the dropped non-dict branch.
  • Coverage stays at 100% (92 tests passing).

Ready for another look. Thanks again!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 24 out of 27 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants