Add Wibeee energy monitoring integration#168419
Add Wibeee energy monitoring integration#168419fquinto wants to merge 83 commits intohome-assistant:devfrom
Conversation
- 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
There was a problem hiding this comment.
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.
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Docs around this can be found here https://developers.home-assistant.io/docs/core/integration/contributing_to_core
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
…scription to sensor module
- Remove 'homeassistant', 'issue_tracker', 'version' (not valid in core) - Add 'network' to dependencies for network functionality
- 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)
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>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| "device": { | ||
| "wibeee_id": device_info.wibeee_id, | ||
| "mac_addr": "**REDACTED**", | ||
| "model": device_info.model, | ||
| "firmware_version": device_info.firmware_version, | ||
| "ip_addr": "**REDACTED**", | ||
| }, |
There was a problem hiding this comment.
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.
| @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 |
There was a problem hiding this comment.
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).
| # 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 |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.
| # 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" |
There was a problem hiding this comment.
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.
…r availability and diagnostics redaction
…figEntry for options flow
…th and use typed ConfigEntry for options flow
| 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, | ||
| }, |
There was a problem hiding this comment.
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.
| DEFAULT_HA_PORT = 8123 | ||
|
|
There was a problem hiding this comment.
Reuse the existing DEFAULT_HA_PORT constant from const.py instead of redefining it here to avoid the two values drifting apart over time.
| raise ConfigEntryNotReady( | ||
| f"Could not fetch initial sensor data from Wibeee at {host}" | ||
| ) | ||
|
|
There was a problem hiding this comment.
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.
| if not isinstance(initial_data, dict): | |
| raise ConfigEntryNotReady( | |
| f"Invalid initial sensor data received from Wibeee at {host}" | |
| ) |
| # 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() | ||
| ] |
There was a problem hiding this comment.
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.
| # 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, | |
| ) | |
| ) |
…e, filter sensor keys
…synchronize test logic
| 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: |
There was a problem hiding this comment.
Validate phase_data is a dict in available before reading sensor keys to avoid attribute errors when coordinator data is malformed.
| 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``. |
There was a problem hiding this comment.
Align the docstring with the actual setup logic, since the code only creates entities for sensor keys present in the initial data.
| 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. |
| return ( | ||
| f"Wibeee {device.mac_addr_short}", | ||
| device.mac_addr_formatted, | ||
| { | ||
| CONF_HOST: data[CONF_HOST], |
There was a problem hiding this comment.
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.
| return WibeeeOptionsFlowHandler() | ||
|
|
||
| async def async_step_reconfigure( | ||
| self, user_input: dict | None = None |
There was a problem hiding this comment.
Add concrete type parameters for user_input in async_step_reconfigure to satisfy the repository mypy setting disallow_any_generics = true.
| self, user_input: dict | None = None | |
| self, user_input: dict[str, Any] | None = None |
| import socket # noqa: PLC0415 | ||
|
|
||
| try: | ||
| resolved_ip = await hass.async_add_executor_job(socket.gethostbyname, host) | ||
| except OSError: | ||
| resolved_ip = host |
There was a problem hiding this comment.
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.
| 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 |
| 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: |
There was a problem hiding this comment.
Guard against non-dict phase payloads before calling .get() so the entity doesn't crash if the coordinator receives malformed data.
| 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: |
There was a problem hiding this comment.
Validate phase_data is a dict in available before reading sensor keys to avoid attribute errors when coordinator data is malformed.
… and strict typing
…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.
| PLATFORMS = [Platform.SENSOR] | ||
|
|
There was a problem hiding this comment.
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.
…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
| 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." |
| 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 |
| if not initial_data: | ||
| raise ConfigEntryNotReady( | ||
| f"Could not fetch initial sensor data from Wibeee at {host}" | ||
| ) | ||
|
|
||
| coordinator.async_set_updated_data(initial_data) |
| coordinator = WibeeeCoordinator( | ||
| hass, | ||
| api, | ||
| config_entry=entry, | ||
| name=f"Wibeee {device_info.mac_addr_short}", | ||
| update_interval=None, |
- 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).
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.
|
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
Linting & translations
Security & robustness
Typing & syntax
Tests
CI noteThe previous run had a single failure in an unrelated flaky test ( Ready for another look. Thanks! |
| if data is None: | ||
| raise UpdateFailed(f"No data received from Wibeee at {self.api.host}") | ||
|
|
||
| if not isinstance(data, dict): |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
| # Type alias: phase_key -> sensor_key -> value |
| """Validate the user input allows us to connect. | ||
|
|
||
| Returns: | ||
| A tuple of (title, unique_id, data). | ||
| """ |
There was a problem hiding this comment.
| """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") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
- 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).
|
Thanks for the careful review @erwindouna! All points addressed in commit e381e63: Coordinator
Config flow
Tests
Ready for another look. Thanks again! |
Proposed change
Add a new integration for
wibeeeenergy 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:
status.xml)The integration is fully local and does not require any cloud connectivity.
Type of change
Additional information
wibeeeintegration home-assistant.io#44815Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: