Skip to content

Commit 60c10c6

Browse files
author
=
committed
Fix manifest.json according to hassfest + Add rx_module implementation + Add tests for easywave
1 parent e4d8b9b commit 60c10c6

23 files changed

+3345
-613
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

homeassistant/components/easywave/__init__.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""The Easywave integration."""
2+
23
from __future__ import annotations
34

5+
from dataclasses import dataclass
46
import logging
57

68
from homeassistant.config_entries import ConfigEntry
79
from homeassistant.const import Platform
810
from homeassistant.core import HomeAssistant
9-
from homeassistant.exceptions import HomeAssistantError
11+
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
1012
from homeassistant.helpers import device_registry as dr, issue_registry as ir
1113

1214
from .const import (
@@ -15,18 +17,29 @@
1517
get_frequency_for_pid,
1618
is_country_allowed_for_frequency,
1719
)
20+
from .coordinator import EasywaveCoordinator
21+
from .transceiver import RX11Transceiver
1822

1923
_LOGGER = logging.getLogger(__name__)
2024

21-
type EasywaveConfigEntry = ConfigEntry[None]
25+
26+
@dataclass
27+
class EasywaveRuntimeData:
28+
"""Runtime data for the Easywave integration."""
29+
30+
coordinator: EasywaveCoordinator
31+
frequency: str
32+
country: str
33+
34+
35+
type EasywaveConfigEntry = ConfigEntry[EasywaveRuntimeData]
2236

2337
PLATFORMS: list[Platform] = [Platform.SENSOR]
2438

2539

2640
async def async_setup_entry(hass: HomeAssistant, entry: EasywaveConfigEntry) -> bool:
2741
"""Set up Easywave from a config entry."""
28-
hass.data.setdefault(DOMAIN, {})
29-
42+
3043
# ── Regulatory compliance check (868 MHz) ──────────────────────────
3144
# The operating frequency is derived from the USB device's PID.
3245
# If the configured HA country is outside the allowed region for that
@@ -37,8 +50,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: EasywaveConfigEntry) ->
3750

3851
if frequency and not is_country_allowed_for_frequency(frequency, country_code):
3952
_LOGGER.warning(
40-
"This hardware operates on %s, which is not permitted in your "
41-
"configured region (%s). Integration disabled for regulatory compliance.",
53+
"This hardware operates on %s, which is not permitted in "
54+
"your configured region (%s). Integration disabled for "
55+
"regulatory compliance",
4256
frequency,
4357
country_code or "unknown",
4458
)
@@ -55,26 +69,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: EasywaveConfigEntry) ->
5569
"country": country_code or "unknown",
5670
},
5771
)
72+
# Return False for regulatory compliance violation (not a setup error)
5873
return False
5974

6075
# If the check passed, make sure any stale repair issue is removed
6176
# (e.g. user changed their country setting).
6277
ir.async_delete_issue(hass, DOMAIN, f"frequency_not_permitted_{entry.entry_id}")
63-
78+
79+
# ── Initialize transceiver and coordinator ──────────────────────────
80+
# Create transceiver instance (will search for USB RX11 device)
81+
transceiver = RX11Transceiver(hass)
82+
83+
# Create coordinator for managing connection lifecycle & offline mode
84+
coordinator = EasywaveCoordinator(hass, transceiver, entry)
85+
86+
# Attempt initial setup (may succeed in offline mode)
87+
if not await coordinator.async_setup():
88+
raise ConfigEntryNotReady("Failed to initialize coordinator")
89+
90+
# Set runtime data for the integration
91+
entry.runtime_data = EasywaveRuntimeData(
92+
coordinator=coordinator,
93+
frequency=frequency or "unknown",
94+
country=country_code or "unknown",
95+
)
96+
6497
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
6598
return True
6699

67100

68101
async def async_unload_entry(hass: HomeAssistant, entry: EasywaveConfigEntry) -> bool:
69102
"""Unload a config entry."""
103+
# Shutdown coordinator
104+
if hasattr(entry, "runtime_data") and entry.runtime_data:
105+
await entry.runtime_data.coordinator.async_shutdown()
106+
70107
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
71108

72109

73110
async def async_remove_config_entry_device(
74111
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
75112
) -> bool:
76-
"""Prevent removing the RX11 gateway device via the UI device menu."""
77-
raise HomeAssistantError(
78-
translation_domain=DOMAIN,
79-
translation_key="cannot_delete_rx11",
80-
)
113+
"""Allow removing devices except the RX11 gateway."""
114+
gateway_identifier = (DOMAIN, f"{config_entry.entry_id}_gateway")
115+
if gateway_identifier in device_entry.identifiers:
116+
raise HomeAssistantError(
117+
translation_domain=DOMAIN,
118+
translation_key="cannot_delete_gateway",
119+
)
120+
return True

homeassistant/components/easywave/config_flow.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
"""Config flow for the Easywave integration."""
2+
23
from __future__ import annotations
34

45
import logging
56
from typing import Any
67

7-
import voluptuous as vol
88
import serial.tools.list_ports
9+
import voluptuous as vol
910

10-
from homeassistant.components import usb
1111
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
1212
from homeassistant.helpers.selector import (
1313
SelectOptionDict,
1414
SelectSelector,
1515
SelectSelectorConfig,
1616
SelectSelectorMode,
1717
)
18+
from homeassistant.helpers.service_info.usb import UsbServiceInfo
1819

1920
from .const import (
2021
CONF_DEVICE_PATH,
@@ -42,7 +43,11 @@ def _find_easywave_devices() -> list[dict[str, Any]]:
4243
if (port.vid, port.pid) in SUPPORTED_USB_IDS:
4344
device_entry = USB_DEVICE_NAMES.get((port.vid, port.pid))
4445
mfr = device_entry["manufacturer"] if device_entry else "ELDAT EaS GmbH"
45-
prod = device_entry["product"] if device_entry else "Unknown Easywave Device"
46+
prod = (
47+
device_entry["product"]
48+
if device_entry
49+
else "Unknown Easywave Device"
50+
)
4651
devices.append(
4752
{
4853
"device": port.device,
@@ -53,7 +58,7 @@ def _find_easywave_devices() -> list[dict[str, Any]]:
5358
"product": prod,
5459
}
5560
)
56-
except Exception: # noqa: BLE001
61+
except Exception:
5762
_LOGGER.exception("Error scanning for Easywave USB devices")
5863
return devices
5964

@@ -129,9 +134,7 @@ async def async_step_detect(
129134
# USB auto-discovery (triggered by manifest `usb` matcher)
130135
# ------------------------------------------------------------------
131136

132-
async def async_step_usb(
133-
self, discovery_info: usb.UsbServiceInfo
134-
) -> ConfigFlowResult:
137+
async def async_step_usb(self, discovery_info: UsbServiceInfo) -> ConfigFlowResult:
135138
"""Handle USB discovery."""
136139
vid = int(discovery_info.vid, 16)
137140
pid = int(discovery_info.pid, 16)
@@ -148,7 +151,7 @@ async def async_step_usb(
148151
device_entry = USB_DEVICE_NAMES.get((vid, pid))
149152
mfr = device_entry["manufacturer"] if device_entry else "ELDAT EaS GmbH"
150153
prod = device_entry["product"] if device_entry else "Unknown Easywave Device"
151-
154+
152155
self._device = {
153156
"device": discovery_info.device,
154157
"vid": vid,

homeassistant/components/easywave/const.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Constants for the Easywave integration."""
2+
23
from __future__ import annotations
34

5+
from datetime import timedelta
46
from typing import Final
57

68
DOMAIN: Final = "easywave"
@@ -21,6 +23,10 @@
2123

2224
SUPPORTED_USB_IDS: Final = frozenset(USB_DEVICE_NAMES.keys())
2325

26+
# ── Coordinator Update Interval ──────────────────────────────────────────────
27+
# Periodic polling interval for USB device reconnection attempts
28+
DEVICE_SCAN_INTERVAL: Final = timedelta(seconds=30)
29+
2430

2531
# ── Config Entry Keys ────────────────────────────────────────────────────────
2632
CONF_DEVICE_PATH: Final = "device_path"
@@ -35,16 +41,46 @@
3541
FREQUENCY_868MHZ: Final = "868 MHz"
3642

3743
FREQUENCY_ALLOWED_COUNTRIES: Final = {
38-
FREQUENCY_868MHZ: frozenset({
39-
# EU Member States (CEPT)
40-
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE",
41-
"GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT",
42-
"RO", "SK", "SI", "ES", "SE",
43-
# CEPT Members (non-EU)
44-
"CH", "NO", "IS", "LI",
45-
# UK (post-Brexit)
46-
"GB", "UK",
47-
}),
44+
FREQUENCY_868MHZ: frozenset(
45+
{
46+
# EU Member States (CEPT)
47+
"AT",
48+
"BE",
49+
"BG",
50+
"HR",
51+
"CY",
52+
"CZ",
53+
"DK",
54+
"EE",
55+
"FI",
56+
"FR",
57+
"DE",
58+
"GR",
59+
"HU",
60+
"IE",
61+
"IT",
62+
"LV",
63+
"LT",
64+
"LU",
65+
"MT",
66+
"NL",
67+
"PL",
68+
"PT",
69+
"RO",
70+
"SK",
71+
"SI",
72+
"ES",
73+
"SE",
74+
# CEPT Members (non-EU)
75+
"CH",
76+
"NO",
77+
"IS",
78+
"LI",
79+
# UK (post-Brexit)
80+
"GB",
81+
"UK",
82+
}
83+
),
4884
}
4985

5086
# Legacy constant for backward compatibility
@@ -64,12 +100,12 @@ def is_country_allowed_for_frequency(frequency: str, country_code: str | None) -
64100
# No country configured — cannot enforce
65101
if country_code is None:
66102
return True
67-
103+
68104
allowed = FREQUENCY_ALLOWED_COUNTRIES.get(frequency)
69105
if allowed is None:
70106
# Unknown frequency — conservative: allow
71107
return True
72-
108+
73109
return country_code.upper() in allowed
74110

75111

0 commit comments

Comments
 (0)