diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 359d711..9863915 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v2
- uses: dmamontov/hass-py-lint@main
with:
- hass-version: ">=2022.3.0"
+ hass-version: ">=2022.4.0"
path: "custom_components/miwifi"
extra-pylint-options: "--disable=unused-argument"
diff --git a/README.md b/README.md
index 9de6ae0..9d04cea 100644
--- a/README.md
+++ b/README.md
@@ -105,9 +105,10 @@ Many more Xiaomi and Redmi routers supported by MiWiFi (OpenWRT - Luci API)
##### Additional
- `misystem/topo_graph` - Topography, auto discovery does not work without it.
+- `xqsystem/check_rom_update` - Getting information about a firmware update
- `xqnetwork/wan_info` - WAN port information.
- `misystem/led` - Interaction with LEDs.
-- `xqnetwork/wifi_detail_all` - Getting information about WiFi adapters
+- `xqnetwork/wifi_diag_detail_all` - Getting information about WiFi adapters
- `xqnetwork/avaliable_channels` - Gets available channels for WiFi adapter
- `xqnetwork/wifi_connect_devices` - Get information about connected devices
- `misystem/devicelist` - More information about connected devices
@@ -116,7 +117,10 @@ Many more Xiaomi and Redmi routers supported by MiWiFi (OpenWRT - Luci API)
##### Action
- `xqsystem/reboot` - Reboot
+- `xqsystem/upgrade_rom` - Firmware update.
+- `xqsystem/flash_permission` - Clear permission. Required only for firmware updates.
- `xqnetwork/set_wifi` - Update WiFi settings. Causes the adapter to reboot.
+- `xqnetwork/set_wifi_without_restart` - Update Guest WiFi settings.
❗ If your router is not listed or not tested, try adding an integration, it will check everything and give a link to create an issue. You just have to click `Submit new issue`
@@ -128,41 +132,41 @@ Many more Xiaomi and Redmi routers supported by MiWiFi (OpenWRT - Luci API)
- 🔴 - Not supported
- ⚪ - Not tested
-| Image | Router | Code | Required API | Additional API | Action API |
-| ------------------------ | -------------------------------------- |:------:|:------------------:|:--------------------------:|:---------------:|
-| ![](images/RA70.png) | **Xiaomi Router Xiaomi AX9000** | RA70 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA72.png) | **Xiaomi Router AX6000** | RA72 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RB06.png) | **Redmi Router AX6000** | RB06 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA80.png) | **Xiaomi Mesh System AX3000** | RA82 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA80.png) | **Xiaomi Router AX3000** | RA80 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RB03.png) | **Redmi Router AX6S** | RB03 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA81.png) | **Redmi Router AX3000** | RA81 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA71.png) | **Redmi Router AX1800** | RA71 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA69.png) | **Redmi Router AX6** | RA69 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA67.png) | **Redmi Router AX5** | RA67 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RA50.png) | **Redmi Router AX5** | RA50 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/AX1800.png) | **Mi Router AX1800** | RM1800 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/AX3600.png) | **Xiaomi AIoT Router AX3600** | R3600 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/RM2100.png) | **Redmi Router AC2100** | RM2100 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/R2100.png) | **Mi Router AC2100** | R2100 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/R1350.png) | **Mi Router 4 Pro** | R1350 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/R2350.png) | **Mi AIoT Router AC2350** | R2350 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢 |
-| ![](images/D01.png) | **Mi Router Mesh** | D01 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R4AC.png) | **Mi Router 4A** | R4AC | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R4A.png) | **Mi Router 4A Gigabit** | R4A | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R4CM.png) | **Mi Router 4C** | R4CM | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R4.png) | **Mi Router 4** | R4 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R4C.png) | **Mi Router 4Q** | R4C | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R3L.png) | **Mi Router 3A** | R3A | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R3L.png) | **Mi Router 3C** | R3L | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R3D.png) | **Mi Router HD** | R3D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/r3p.png) | **Mi Router Pro** | R3P | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R3.png) | **Mi Router 3** | R3 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R3.png) | **Mi Router 3G** | R3G | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R1CL.png) | **Mi Router Lite** | R1CL | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R1C.png) | **Mi Router Mini** | R1CM | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R2D.png) | **Mi Router R2D** | R2D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
-| ![](images/R1D.png) | **Mi Router R1D** | R1D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢 |
+| Image | Router | Code | Required | Additional | Action |
+| ------------------------ | -------------------------------------- |:------:|:------------------:|:----------------------------:|:--------------------:|
+| ![](images/RA70.png) | **Xiaomi Router Xiaomi AX9000** | RA70 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA72.png) | **Xiaomi Router AX6000** | RA72 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RB06.png) | **Redmi Router AX6000** | RB06 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA80.png) | **Xiaomi Mesh System AX3000** | RA82 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA80.png) | **Xiaomi Router AX3000** | RA80 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RB03.png) | **Redmi Router AX6S** | RB03 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA81.png) | **Redmi Router AX3000** | RA81 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA71.png) | **Redmi Router AX1800** | RA71 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA69.png) | **Redmi Router AX6** | RA69 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA67.png) | **Redmi Router AX5** | RA67 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RA50.png) | **Redmi Router AX5** | RA50 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/AX1800.png) | **Mi Router AX1800** | RM1800 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/AX3600.png) | **Xiaomi AIoT Router AX3600** | R3600 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/RM2100.png) | **Redmi Router AC2100** | RM2100 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/R2100.png) | **Mi Router AC2100** | R2100 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/R1350.png) | **Mi Router 4 Pro** | R1350 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/R2350.png) | **Mi AIoT Router AC2350** | R2350 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢 |
+| ![](images/D01.png) | **Mi Router Mesh** | D01 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R4AC.png) | **Mi Router 4A** | R4AC | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R4A.png) | **Mi Router 4A Gigabit** | R4A | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R4CM.png) | **Mi Router 4C** | R4CM | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R4.png) | **Mi Router 4** | R4 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R4C.png) | **Mi Router 4Q** | R4C | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R3L.png) | **Mi Router 3A** | R3A | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R3L.png) | **Mi Router 3C** | R3L | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R3D.png) | **Mi Router HD** | R3D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/r3p.png) | **Mi Router Pro** | R3P | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R3.png) | **Mi Router 3** | R3 | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R3.png) | **Mi Router 3G** | R3G | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R1CL.png) | **Mi Router Lite** | R1CL | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R1C.png) | **Mi Router Mini** | R1CM | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R2D.png) | **Mi Router R2D** | R2D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
+| ![](images/R1D.png) | **Mi Router R1D** | R1D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢🟢 |
## Diagnostics
You will need to obtain diagnostic data to search for a problem or before creating an issue.
diff --git a/custom_components/miwifi/__init__.py b/custom_components/miwifi/__init__.py
index 4d9d8ca..9a2abeb 100644
--- a/custom_components/miwifi/__init__.py
+++ b/custom_components/miwifi/__init__.py
@@ -14,6 +14,7 @@
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, Event, CALLBACK_TYPE
+from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.storage import Store
from .const import (
@@ -84,18 +85,18 @@ async def async_start(with_sleep: bool = False) -> None:
:param with_sleep: bool
"""
- await _updater.update(True)
+ await _updater.async_config_entry_first_refresh()
+ if not _updater.last_update_success:
+ if _updater.last_exception is not None:
+ raise PlatformNotReady from _updater.last_exception
+
+ raise PlatformNotReady
if with_sleep:
await asyncio.sleep(DEFAULT_SLEEP)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
- async def async_stop(event: Event) -> None:
- """Async stop"""
-
- await _updater.async_stop()
-
if is_new:
await async_start()
await asyncio.sleep(DEFAULT_SLEEP)
@@ -105,6 +106,11 @@ async def async_stop(event: Event) -> None:
lambda: hass.async_create_task(async_start(True)),
)
+ async def async_stop(event: Event) -> None:
+ """Async stop"""
+
+ await _updater.async_stop()
+
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop)
return True
diff --git a/custom_components/miwifi/binary_sensor.py b/custom_components/miwifi/binary_sensor.py
index e123b8e..780e930 100644
--- a/custom_components/miwifi/binary_sensor.py
+++ b/custom_components/miwifi/binary_sensor.py
@@ -85,10 +85,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error(
- "Failed to initialize binary sensor: Missing mac address. Restart HASS."
- )
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize binary sensor.")
+
+ return
entities: list[MiWifiBinarySensor] = [
MiWifiBinarySensor(
diff --git a/custom_components/miwifi/button.py b/custom_components/miwifi/button.py
index ff2ba3b..34ae19c 100644
--- a/custom_components/miwifi/button.py
+++ b/custom_components/miwifi/button.py
@@ -57,8 +57,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error("Failed to initialize button: Missing mac address. Restart HASS.")
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize button.")
+
+ return
entities: list[MiWifiButton] = [
MiWifiButton(
diff --git a/custom_components/miwifi/camera.py b/custom_components/miwifi/camera.py
index ab2db98..4ef8f10 100644
--- a/custom_components/miwifi/camera.py
+++ b/custom_components/miwifi/camera.py
@@ -55,9 +55,11 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
+ if not updater.last_update_success:
_LOGGER.error("Failed to initialize camera: Missing mac address. Restart HASS.")
+ return
+
entities: list[MiWifiCamera] = [
MiWifiCamera(
f"{config_entry.entry_id}-{description.key}",
diff --git a/custom_components/miwifi/const.py b/custom_components/miwifi/const.py
index baef84b..28fb1ab 100644
--- a/custom_components/miwifi/const.py
+++ b/custom_components/miwifi/const.py
@@ -19,6 +19,7 @@
Platform.SELECT,
Platform.DEVICE_TRACKER,
Platform.CAMERA,
+ Platform.UPDATE,
]
"""Discovery const"""
@@ -91,6 +92,7 @@
ATTR_WIFI_2_4_DATA: Final = "wifi_2_4_data"
ATTR_WIFI_5_0_DATA: Final = "wifi_5_0_data"
ATTR_WIFI_5_0_GAME_DATA: Final = "wifi_5_0_game_data"
+ATTR_WIFI_GUEST_DATA: Final = "wifi_guest_data"
ATTR_WIFI_ADAPTER_LENGTH: Final = "wifi_adapter_length"
@@ -162,6 +164,9 @@
ATTR_SWITCH_WIFI_5_0_GAME: Final = "wifi_5_0_game"
ATTR_SWITCH_WIFI_5_0_GAME_NAME: Final = f"{ATTR_WIFI_NAME} 5G game"
+ATTR_SWITCH_WIFI_GUEST: Final = "wifi_guest"
+ATTR_SWITCH_WIFI_GUEST_NAME: Final = f"{ATTR_WIFI_NAME} guest"
+
"""Select attributes"""
ATTR_SELECT_WIFI_2_4_CHANNEL: Final = "wifi_2_4_channel"
ATTR_SELECT_WIFI_2_4_CHANNELS: Final = "wifi_2_4_channels"
@@ -213,4 +218,17 @@
ATTR_TRACKER_DOWN_SPEED: Final = "down_speed"
ATTR_TRACKER_UP_SPEED: Final = "up_speed"
ATTR_TRACKER_LAST_ACTIVITY: Final = "last_activity"
+ATTR_TRACKER_OPTIONAL_MAC: Final = "optional_mac"
+
+"""Update attributes"""
+ATTR_UPDATE_FIRMWARE: Final = "firmware"
+ATTR_UPDATE_FIRMWARE_NAME: Final = "Firmware update"
+
+ATTR_UPDATE_TITLE: Final = "title"
+ATTR_UPDATE_CURRENT_VERSION: Final = "current_version"
+ATTR_UPDATE_LATEST_VERSION: Final = "latest_version"
+ATTR_UPDATE_RELEASE_URL: Final = "release_url"
+ATTR_UPDATE_DOWNLOAD_URL: Final = "download_url"
+ATTR_UPDATE_FILE_SIZE: Final = "file_size"
+ATTR_UPDATE_FILE_HASH: Final = "file_hash"
# fmt: on
diff --git a/custom_components/miwifi/device_tracker.py b/custom_components/miwifi/device_tracker.py
index b98b9fe..07cf67e 100644
--- a/custom_components/miwifi/device_tracker.py
+++ b/custom_components/miwifi/device_tracker.py
@@ -42,6 +42,7 @@
ATTR_TRACKER_DOWN_SPEED,
ATTR_TRACKER_UP_SPEED,
ATTR_TRACKER_LAST_ACTIVITY,
+ ATTR_TRACKER_OPTIONAL_MAC,
)
from .enum import Connection
from .helper import generate_entity_id, parse_last_activity, pretty_size
@@ -55,6 +56,7 @@
ATTR_TRACKER_SIGNAL,
ATTR_TRACKER_DOWN_SPEED,
ATTR_TRACKER_UP_SPEED,
+ ATTR_TRACKER_OPTIONAL_MAC,
]
CONFIGURATION_PORTS: Final = [80, 443]
@@ -77,6 +79,11 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize device tracker.")
+
+ return
+
@callback
def add_device(device: dict) -> None:
"""Add device.
@@ -303,6 +310,17 @@ def device_info(self) -> DeviceInfo: # pylint: disable=overridden-final-method
:return DeviceInfo: Device Info
"""
+ _optional_mac = self._device.get(ATTR_TRACKER_OPTIONAL_MAC, None)
+ if _optional_mac is not None:
+ return DeviceInfo(
+ connections={
+ (dr.CONNECTION_NETWORK_MAC, self.mac_address),
+ (dr.CONNECTION_NETWORK_MAC, _optional_mac),
+ },
+ identifiers={(DOMAIN, self.mac_address)},
+ name=self._attr_name,
+ )
+
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self.mac_address)},
identifiers={(DOMAIN, self.mac_address)},
diff --git a/custom_components/miwifi/enum.py b/custom_components/miwifi/enum.py
index 3016703..3757ec9 100644
--- a/custom_components/miwifi/enum.py
+++ b/custom_components/miwifi/enum.py
@@ -8,6 +8,7 @@
ATTR_SWITCH_WIFI_2_4,
ATTR_SWITCH_WIFI_5_0,
ATTR_SWITCH_WIFI_5_0_GAME,
+ ATTR_SWITCH_WIFI_GUEST,
)
@@ -105,6 +106,7 @@ def __str__(self) -> str:
WL0 = "wl0", ATTR_SWITCH_WIFI_5_0
WL1 = "wl1", ATTR_SWITCH_WIFI_2_4
WL2 = "wl2", ATTR_SWITCH_WIFI_5_0_GAME
+ WL14 = "wl14", ATTR_SWITCH_WIFI_GUEST
class Wifi(IntEnum):
diff --git a/custom_components/miwifi/light.py b/custom_components/miwifi/light.py
index 3127267..2cb9dfa 100644
--- a/custom_components/miwifi/light.py
+++ b/custom_components/miwifi/light.py
@@ -67,8 +67,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error("Failed to initialize light: Missing mac address. Restart HASS.")
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize light.")
+
+ return
entities: list[MiWifiLight] = [
MiWifiLight(
diff --git a/custom_components/miwifi/luci.py b/custom_components/miwifi/luci.py
index ff0d6c0..9b5446d 100644
--- a/custom_components/miwifi/luci.py
+++ b/custom_components/miwifi/luci.py
@@ -29,7 +29,7 @@
DIAGNOSTIC_MESSAGE,
DIAGNOSTIC_CONTENT,
)
-from .exceptions import LuciConnectionException, LuciTokenException
+from .exceptions import LuciException, LuciConnectionException, LuciTokenException
_LOGGER = logging.getLogger(__name__)
@@ -134,13 +134,18 @@ async def logout(self) -> None:
self._debug("Logout error", _url, _e, _method)
async def get(
- self, path: str, query_params: dict | None = None, use_stok: bool = True
+ self,
+ path: str,
+ query_params: dict | None = None,
+ use_stok: bool = True,
+ errors: dict[int, str] | None = None,
) -> dict:
"""GET method.
:param path: str: api method
:param query_params: dict | None: Data
:param use_stok: bool: is use stack
+ :param errors: dict[int, str] | None: errors list
:return dict: dict with api data.
"""
@@ -165,6 +170,9 @@ async def get(
if "code" not in _data or _data["code"] > 0:
self._debug("Invalid error code received", _url, _data, path)
+ if "code" in _data and errors is not None and _data["code"] in errors:
+ raise LuciException(errors[_data["code"]])
+
raise LuciTokenException("Invalid error code received")
return _data
@@ -226,12 +234,12 @@ async def wifi_ap_signal(self) -> dict:
return await self.get("xqnetwork/wifiap_signal")
async def wifi_detail_all(self) -> dict:
- """xqnetwork/wifi_detail_all method.
+ """xqnetwork/wifi_diag_detail_all method.
:return dict: dict with api data.
"""
- return await self.get("xqnetwork/wifi_detail_all")
+ return await self.get("xqnetwork/wifi_diag_detail_all")
async def set_wifi(self, data: dict) -> dict:
"""xqnetwork/set_wifi method.
@@ -242,6 +250,15 @@ async def set_wifi(self, data: dict) -> dict:
return await self.get("xqnetwork/set_wifi", data)
+ async def set_guest_wifi(self, data: dict) -> dict:
+ """xqnetwork/set_wifi_without_restart method.
+
+ :param data: dict: Adapter data
+ :return dict: dict with api data.
+ """
+
+ return await self.get("xqnetwork/set_wifi_without_restart", data)
+
async def avaliable_channels(self, index: int = 1) -> dict:
"""xqnetwork/avaliable_channels method.
@@ -296,6 +313,41 @@ async def wifi_connect_devices(self) -> dict:
return await self.get("xqnetwork/wifi_connect_devices")
+ async def rom_update(self) -> dict:
+ """xqsystem/check_rom_update method.
+
+ :return dict: dict with api data.
+ """
+
+ return await self.get("xqsystem/check_rom_update")
+
+ async def rom_upgrade(self, data: dict) -> dict:
+ """xqsystem/upgrade_rom method.
+
+ :param data: dict: Rom data
+ :return dict: dict with api data.
+ """
+
+ return await self.get(
+ "xqsystem/upgrade_rom",
+ data,
+ errors={
+ 6: "Download failed",
+ 7: "No disk space",
+ 8: "Download failed",
+ 9: "Upgrade package verification failed",
+ 10: "Failed to flash",
+ },
+ )
+
+ async def flash_permission(self) -> dict:
+ """xqsystem/flash_permission method.
+
+ :return dict: dict with api data.
+ """
+
+ return await self.get("xqsystem/flash_permission")
+
async def image(self, hardware: str) -> bytes | None:
"""router image method.
diff --git a/custom_components/miwifi/manifest.json b/custom_components/miwifi/manifest.json
index 496ddc3..811a03d 100644
--- a/custom_components/miwifi/manifest.json
+++ b/custom_components/miwifi/manifest.json
@@ -1,7 +1,7 @@
{
"domain": "miwifi",
"name": "MiWiFi",
- "version": "2.4.5",
+ "version": "2.5.0",
"documentation": "https://github.com/dmamontov/hass-miwifi/blob/main/README.md",
"issue_tracker": "https://github.com/dmamontov/hass-miwifi/issues",
"config_flow": true,
diff --git a/custom_components/miwifi/select.py b/custom_components/miwifi/select.py
index 4bfacbe..c41c1f0 100644
--- a/custom_components/miwifi/select.py
+++ b/custom_components/miwifi/select.py
@@ -151,8 +151,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error("Failed to initialize switch: Missing mac address. Restart HASS.")
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize select.")
+
+ return
entities: list[MiWifiSelect] = []
for description in MIWIFI_SELECTS:
diff --git a/custom_components/miwifi/self_check.py b/custom_components/miwifi/self_check.py
index 9d518be..bb2915e 100644
--- a/custom_components/miwifi/self_check.py
+++ b/custom_components/miwifi/self_check.py
@@ -16,11 +16,12 @@
SELF_CHECK_METHODS: Final = {
"misystem/status": "status",
+ "xqsystem/check_rom_update": "rom_update",
"xqnetwork/mode": "mode",
"misystem/topo_graph": "topo_graph",
"xqnetwork/wan_info": "wan_info",
"misystem/led": "led",
- "xqnetwork/wifi_detail_all": "wifi_detail_all",
+ "xqnetwork/wifi_diag_detail_all": "wifi_detail_all",
"xqnetwork/avaliable_channels": "avaliable_channels",
"xqnetwork/wifi_connect_devices": "wifi_connect_devices",
"misystem/devicelist": "device_list",
@@ -42,8 +43,11 @@ async def async_self_check(hass: HomeAssistant, client: LuciClient, model: str)
data = {
"xqsystem/login": "🟢",
"xqsystem/init_info": "🟢",
- "xqsystem/reboot": "🟢",
- "xqnetwork/set_wifi": "🟢",
+ "xqsystem/reboot": "⚪",
+ "xqnetwork/set_wifi": "⚪",
+ "xqnetwork/set_wifi_without_restart": "⚪",
+ "xqsystem/upgrade_rom": "⚪",
+ "xqsystem/flash_permission": "⚪",
}
for code, method in SELF_CHECK_METHODS.items():
diff --git a/custom_components/miwifi/sensor.py b/custom_components/miwifi/sensor.py
index 5f206f2..5ece4a6 100644
--- a/custom_components/miwifi/sensor.py
+++ b/custom_components/miwifi/sensor.py
@@ -211,8 +211,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error("Failed to initialize sensor: Missing mac address. Restart HASS.")
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize sensor.")
+
+ return
entities: list[MiWifiSensor] = []
for description in MIWIFI_SENSORS:
diff --git a/custom_components/miwifi/switch.py b/custom_components/miwifi/switch.py
index 673373f..bd0fcdc 100644
--- a/custom_components/miwifi/switch.py
+++ b/custom_components/miwifi/switch.py
@@ -39,6 +39,9 @@
ATTR_SWITCH_WIFI_5_0_GAME,
ATTR_SWITCH_WIFI_5_0_GAME_NAME,
ATTR_WIFI_5_0_GAME_DATA,
+ ATTR_SWITCH_WIFI_GUEST,
+ ATTR_SWITCH_WIFI_GUEST_NAME,
+ ATTR_WIFI_GUEST_DATA,
)
from .enum import Wifi
from .exceptions import LuciException
@@ -49,6 +52,7 @@
ATTR_SWITCH_WIFI_2_4: ATTR_WIFI_2_4_DATA,
ATTR_SWITCH_WIFI_5_0: ATTR_WIFI_5_0_DATA,
ATTR_SWITCH_WIFI_5_0_GAME: ATTR_WIFI_5_0_GAME_DATA,
+ ATTR_SWITCH_WIFI_GUEST: ATTR_WIFI_GUEST_DATA,
}
ICONS: Final = {
@@ -58,6 +62,8 @@
f"{ATTR_SWITCH_WIFI_5_0}_{STATE_OFF}": "mdi:wifi-off",
f"{ATTR_SWITCH_WIFI_5_0_GAME}_{STATE_ON}": "mdi:wifi",
f"{ATTR_SWITCH_WIFI_5_0_GAME}_{STATE_OFF}": "mdi:wifi-off",
+ f"{ATTR_SWITCH_WIFI_GUEST}_{STATE_ON}": "mdi:wifi-lock-open",
+ f"{ATTR_SWITCH_WIFI_GUEST}_{STATE_OFF}": "mdi:wifi-off",
}
MIWIFI_SWITCHES: tuple[SwitchEntityDescription, ...] = (
@@ -82,6 +88,13 @@
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=True,
),
+ SwitchEntityDescription(
+ key=ATTR_SWITCH_WIFI_GUEST,
+ name=ATTR_SWITCH_WIFI_GUEST_NAME,
+ icon=ICONS[f"{ATTR_SWITCH_WIFI_GUEST}_{STATE_ON}"],
+ entity_category=EntityCategory.CONFIG,
+ entity_registry_enabled_default=False,
+ ),
)
_LOGGER = logging.getLogger(__name__)
@@ -102,8 +115,10 @@ async def async_setup_entry(
data: dict = hass.data[DOMAIN][config_entry.entry_id]
updater: LuciUpdater = data[UPDATER]
- if not updater.data.get(ATTR_DEVICE_MAC_ADDRESS, False):
- _LOGGER.error("Failed to initialize switch: Missing mac address. Restart HASS.")
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize switch.")
+
+ return
entities: list[MiWifiSwitch] = []
for description in MIWIFI_SWITCHES:
@@ -273,6 +288,20 @@ async def _wifi_5_0_game_off(self) -> None:
await self._async_update_wifi_adapter(data)
+ async def _wifi_guest_on(self) -> None:
+ """Wifi 2.4G on action"""
+
+ data: dict = {"wifiIndex": 3, "on": 1}
+
+ await self._async_update_guest_wifi(data)
+
+ async def _wifi_guest_off(self) -> None:
+ """Wifi 2.4G off action"""
+
+ data: dict = {"wifiIndex": 3, "on": 0}
+
+ await self._async_update_guest_wifi(data)
+
async def _async_update_wifi_adapter(self, data: dict) -> None:
"""Update wifi adapter
@@ -287,6 +316,20 @@ async def _async_update_wifi_adapter(self, data: dict) -> None:
except LuciException as _e:
_LOGGER.debug("WiFi update error: %r", _e)
+ async def _async_update_guest_wifi(self, data: dict) -> None:
+ """Update guest wifi
+
+ :param data: dict: Guest data
+ """
+
+ new_data: dict = self._wifi_data | data
+
+ try:
+ await self._updater.luci.set_guest_wifi(new_data)
+ self._wifi_data = new_data
+ except LuciException as _e:
+ _LOGGER.debug("WiFi update error: %r", _e)
+
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on action
diff --git a/custom_components/miwifi/update.py b/custom_components/miwifi/update.py
new file mode 100644
index 0000000..5feab17
--- /dev/null
+++ b/custom_components/miwifi/update.py
@@ -0,0 +1,310 @@
+"""Update component."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from typing import Final, Any
+
+from homeassistant.components.camera import ENTITY_ID_FORMAT as CAMERA_ENTITY_ID_FORMAT
+from homeassistant.components.update import (
+ ENTITY_ID_FORMAT,
+ ATTR_IN_PROGRESS,
+ UpdateEntityDescription,
+ UpdateEntity,
+ UpdateEntityFeature,
+ UpdateDeviceClass,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+from homeassistant.helpers.update_coordinator import CoordinatorEntity
+
+from .const import (
+ DOMAIN,
+ UPDATER,
+ ATTRIBUTION,
+ ATTR_DEVICE_MAC_ADDRESS,
+ ATTR_STATE,
+ ATTR_CAMERA_IMAGE_NAME,
+ ATTR_UPDATE_FIRMWARE,
+ ATTR_UPDATE_FIRMWARE_NAME,
+ ATTR_UPDATE_TITLE,
+ ATTR_UPDATE_CURRENT_VERSION,
+ ATTR_UPDATE_LATEST_VERSION,
+ ATTR_UPDATE_RELEASE_URL,
+ ATTR_UPDATE_DOWNLOAD_URL,
+ ATTR_UPDATE_FILE_SIZE,
+ ATTR_UPDATE_FILE_HASH,
+)
+from .exceptions import LuciException
+from .helper import generate_entity_id
+from .updater import LuciUpdater
+
+FIRMWARE_UPDATE_WAIT: Final = 180
+FIRMWARE_UPDATE_RETRY: Final = 721
+
+ATTR_CHANGES: Final = [
+ ATTR_UPDATE_TITLE,
+ ATTR_UPDATE_CURRENT_VERSION,
+ ATTR_UPDATE_LATEST_VERSION,
+ ATTR_UPDATE_RELEASE_URL,
+ ATTR_UPDATE_DOWNLOAD_URL,
+ ATTR_UPDATE_FILE_SIZE,
+ ATTR_UPDATE_FILE_HASH,
+]
+
+MAP_FEATURE: Final = {
+ ATTR_UPDATE_FIRMWARE: UpdateEntityFeature.INSTALL
+ | UpdateEntityFeature.RELEASE_NOTES
+}
+
+MAP_NOTES: Final = {
+ ATTR_UPDATE_FIRMWARE: "\n\n"
+ + "The firmware update takes an average of 3 to 15 minutes."
+ + "\n\n"
+}
+
+MIWIFI_UPDATES: tuple[UpdateEntityDescription, ...] = (
+ UpdateEntityDescription(
+ key=ATTR_UPDATE_FIRMWARE,
+ name=ATTR_UPDATE_FIRMWARE_NAME,
+ device_class=UpdateDeviceClass.FIRMWARE,
+ entity_category=EntityCategory.CONFIG,
+ entity_registry_enabled_default=True,
+ ),
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ config_entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up MiWifi update entry.
+
+ :param hass: HomeAssistant: Home Assistant object
+ :param config_entry: ConfigEntry: ConfigEntry object
+ :param async_add_entities: AddEntitiesCallback: AddEntitiesCallback callback object
+ """
+
+ data: dict = hass.data[DOMAIN][config_entry.entry_id]
+ updater: LuciUpdater = data[UPDATER]
+
+ if not updater.last_update_success:
+ _LOGGER.error("Failed to initialize update.")
+
+ return
+
+ entities: list[MiWifiUpdate] = []
+ for description in MIWIFI_UPDATES:
+ if description.key == ATTR_UPDATE_FIRMWARE and len(description.key) == 0:
+ continue
+
+ entities.append(
+ MiWifiUpdate(
+ f"{config_entry.entry_id}-{description.key}",
+ description,
+ updater,
+ )
+ )
+
+ if len(entities) > 0:
+ async_add_entities(entities)
+
+
+class MiWifiUpdate(UpdateEntity, CoordinatorEntity):
+ """MiWifi update entry."""
+
+ _attr_attribution: str = ATTRIBUTION
+
+ _update_data: dict[str, Any]
+
+ def __init__(
+ self,
+ unique_id: str,
+ description: UpdateEntityDescription,
+ updater: LuciUpdater,
+ ) -> None:
+ """Initialize update.
+
+ :param unique_id: str: Unique ID
+ :param description: UpdateEntityDescription: UpdateEntityDescription object
+ :param updater: LuciUpdater: Luci updater object
+ """
+
+ CoordinatorEntity.__init__(self, coordinator=updater)
+
+ self.entity_description = description
+ self._updater = updater
+
+ self.entity_id = generate_entity_id(
+ ENTITY_ID_FORMAT,
+ updater.data.get(ATTR_DEVICE_MAC_ADDRESS, updater.ip),
+ description.name,
+ )
+
+ if description.key in MAP_FEATURE:
+ self._attr_supported_features = MAP_FEATURE[description.key]
+
+ self._attr_name = description.name
+ self._attr_unique_id = unique_id
+ self._attr_device_info = updater.device_info
+
+ self._update_data = updater.data.get(description.key, {})
+
+ # fmt: off
+ self._attr_available = updater.data.get(ATTR_STATE, False) \
+ and len(self._update_data) > 0
+ # fmt: on
+
+ self._attr_title = self._update_data.get(ATTR_UPDATE_TITLE, None)
+ self._attr_installed_version = self._update_data.get(
+ ATTR_UPDATE_CURRENT_VERSION, None
+ )
+ self._attr_latest_version = self._update_data.get(
+ ATTR_UPDATE_LATEST_VERSION, None
+ )
+ self._attr_release_url = self._update_data.get(ATTR_UPDATE_RELEASE_URL, None)
+
+ async def async_added_to_hass(self) -> None:
+ """When entity is added to hass."""
+
+ await CoordinatorEntity.async_added_to_hass(self)
+
+ _camera_state = self.hass.states.get(
+ generate_entity_id(
+ CAMERA_ENTITY_ID_FORMAT,
+ self._updater.data.get(ATTR_DEVICE_MAC_ADDRESS, self._updater.ip),
+ ATTR_CAMERA_IMAGE_NAME,
+ )
+ )
+
+ if not _camera_state:
+ return
+
+ self._attr_entity_picture = _camera_state.attributes.get("entity_picture", None)
+
+ @property
+ def available(self) -> bool:
+ """Is available
+
+ :return bool: Is available
+ """
+
+ return self._attr_available and self.coordinator.last_update_success
+
+ @property
+ def entity_picture(self) -> str | None:
+ """Return the entity picture to use in the frontend."""
+
+ if self._attr_entity_picture is not None:
+ return self._attr_entity_picture
+
+ if self.platform is None:
+ return None
+
+ return (
+ f"https://brands.home-assistant.io/_/{self.platform.platform_name}/icon.png"
+ )
+
+ def _handle_coordinator_update(self) -> None:
+ """Update state."""
+
+ if self.state_attributes.get(ATTR_IN_PROGRESS, False):
+ return
+
+ _update_data: dict[str, Any] = self._updater.data.get(
+ self.entity_description.key, {}
+ )
+
+ # fmt: off
+ is_available = self._updater.data.get(ATTR_STATE, False) \
+ and len(_update_data) > 0
+ # fmt: on
+
+ is_update = False
+ for attr in ATTR_CHANGES:
+ if self._update_data.get(attr, None) != _update_data.get(attr, None):
+ is_update = True
+
+ break
+
+ if self._attr_available == is_available and not is_update: # type: ignore
+ return
+
+ self._attr_available = is_available
+ self._update_data = _update_data
+
+ self._attr_title = self._update_data.get(ATTR_UPDATE_TITLE, None)
+ self._attr_installed_version = self._update_data.get(
+ ATTR_UPDATE_CURRENT_VERSION, None
+ )
+ self._attr_latest_version = self._update_data.get(
+ ATTR_UPDATE_LATEST_VERSION, None
+ )
+ self._attr_release_url = self._update_data.get(ATTR_UPDATE_RELEASE_URL, None)
+
+ self.async_write_ha_state()
+
+ async def _firmware_install(self) -> None:
+ """Install firmware"""
+
+ try:
+ await self._updater.luci.rom_upgrade(
+ {
+ "url": self._update_data.get(ATTR_UPDATE_DOWNLOAD_URL),
+ "filesize": self._update_data.get(ATTR_UPDATE_FILE_SIZE),
+ "hash": self._update_data.get(ATTR_UPDATE_FILE_HASH),
+ "needpermission": 1,
+ }
+ )
+ except LuciException as _e:
+ raise HomeAssistantError(str(_e)) from _e
+
+ try:
+ await self._updater.luci.flash_permission()
+ except LuciException as _e:
+ _LOGGER.debug("Clear permission error: %r", _e)
+
+ await asyncio.sleep(FIRMWARE_UPDATE_WAIT)
+
+ for _retry in range(1, FIRMWARE_UPDATE_RETRY):
+ if self._updater.data.get(ATTR_STATE, False):
+ break
+
+ await asyncio.sleep(1)
+
+ async def async_install(
+ self, version: str | None, backup: bool, **kwargs: Any
+ ) -> None:
+ """Install an update.
+
+ :param version: str | None
+ :param backup: bool
+ :param kwargs: Any: Any arguments
+ """
+
+ action = getattr(self, f"_{self.entity_description.key}_install")
+
+ if action:
+ await action()
+
+ self._attr_installed_version = self._attr_latest_version
+
+ self.async_write_ha_state()
+
+ async def async_release_notes(self) -> str | None:
+ """Release notes
+
+ :return str | None: Notes
+ """
+
+ if self.entity_description.key not in MAP_NOTES:
+ return None
+
+ return MAP_NOTES[self.entity_description.key]
diff --git a/custom_components/miwifi/updater.py b/custom_components/miwifi/updater.py
index b8f967a..4c046a2 100644
--- a/custom_components/miwifi/updater.py
+++ b/custom_components/miwifi/updater.py
@@ -72,6 +72,15 @@
ATTR_TRACKER_LAST_ACTIVITY,
ATTR_TRACKER_DOWN_SPEED,
ATTR_TRACKER_UP_SPEED,
+ ATTR_TRACKER_OPTIONAL_MAC,
+ ATTR_UPDATE_FIRMWARE,
+ ATTR_UPDATE_TITLE,
+ ATTR_UPDATE_CURRENT_VERSION,
+ ATTR_UPDATE_LATEST_VERSION,
+ ATTR_UPDATE_RELEASE_URL,
+ ATTR_UPDATE_DOWNLOAD_URL,
+ ATTR_UPDATE_FILE_SIZE,
+ ATTR_UPDATE_FILE_HASH,
)
from .enum import (
Model,
@@ -88,6 +97,7 @@
PREPARE_METHODS: Final = [
"init",
"status",
+ "rom_update",
"mode",
"wan",
"led",
@@ -196,11 +206,6 @@ def __init__(
self.data: dict[str, Any] = {}
self.devices: dict[str, dict[str, Any]] = {}
- hass.loop.call_later(
- DEFAULT_CALL_DELAY,
- lambda: hass.async_create_task(self._async_load_manufacturers()),
- )
-
async def async_stop(self) -> None:
"""Stop updater"""
@@ -219,32 +224,29 @@ def _update_interval(self) -> timedelta:
return timedelta(seconds=self._scan_interval)
- async def update(self, is_force: bool = False, retry: int = 1) -> dict:
+ async def update(self, retry: int = 1) -> dict:
"""Update miwifi information.
- :param is_force: bool: Force relogin
:param retry: int: Retry count
:return dict: dict with luci data.
"""
+ await self._async_load_manufacturers()
+
+ _is_before_reauthorization: bool = self._is_reauthorization
_err: LuciException | None = None
try:
- if self._is_reauthorization or self._is_only_login or is_force:
- if is_force:
+ if self._is_reauthorization or self._is_only_login or self._is_first_update:
+ if self._is_first_update:
await self.luci.logout()
await asyncio.sleep(DEFAULT_CALL_DELAY)
await self.luci.login()
- self.code = codes.OK
-
for method in PREPARE_METHODS:
- if not self._is_only_login or is_force or method == "init":
+ if not self._is_only_login or self._is_first_update or method == "init":
await self._async_prepare(method, self.data)
-
- if not self._is_only_login or is_force:
- self._is_first_update = False
except LuciConnectionException as _e:
_err = _e
@@ -255,10 +257,24 @@ async def update(self, is_force: bool = False, retry: int = 1) -> dict:
self._is_reauthorization = True
self.code = codes.FORBIDDEN
+ else:
+ self.code = codes.OK
+
+ self._is_reauthorization = False
+
+ if self._is_first_update:
+ self._is_first_update = False
self.data[ATTR_STATE] = codes.is_success(self.code)
- if is_force and not self.data[ATTR_STATE]:
+ if (
+ not self._is_first_update
+ and not _is_before_reauthorization
+ and self._is_reauthorization
+ ):
+ self.data[ATTR_STATE] = True
+
+ if self._is_first_update and not self.data[ATTR_STATE]:
if retry > DEFAULT_RETRY and _err is not None:
raise _err
@@ -272,7 +288,7 @@ async def update(self, is_force: bool = False, retry: int = 1) -> dict:
await asyncio.sleep(retry)
- return await self.update(True, retry + 1)
+ return await self.update(retry + 1)
if not self._is_only_login:
self._clean_devices()
@@ -416,12 +432,11 @@ async def _async_prepare_status(self, data: dict) -> None:
response: dict = await self.luci.status()
- if (
- "hardware" in response
- and isinstance(response["hardware"], dict)
- and "mac" in response["hardware"]
- ):
- data[ATTR_DEVICE_MAC_ADDRESS] = response["hardware"]["mac"]
+ if "hardware" in response and isinstance(response["hardware"], dict):
+ if "mac" in response["hardware"]:
+ data[ATTR_DEVICE_MAC_ADDRESS] = response["hardware"]["mac"]
+ if "version" in response["hardware"]:
+ data[ATTR_UPDATE_CURRENT_VERSION] = response["hardware"]["version"]
if "upTime" in response:
data[ATTR_SENSOR_UPTIME] = str(
@@ -453,6 +468,41 @@ async def _async_prepare_status(self, data: dict) -> None:
) if "upspeed" in response["wan"] else 0
# fmt: on
+ async def _async_prepare_rom_update(self, data: dict) -> None:
+ """Prepare rom update.
+
+ :param data: dict
+ """
+
+ if ATTR_UPDATE_CURRENT_VERSION not in data:
+ return
+
+ response: dict = await self.luci.rom_update()
+
+ _rom_info: dict = {
+ ATTR_UPDATE_CURRENT_VERSION: data[ATTR_UPDATE_CURRENT_VERSION],
+ ATTR_UPDATE_LATEST_VERSION: data[ATTR_UPDATE_CURRENT_VERSION],
+ ATTR_UPDATE_TITLE: f"{data.get(ATTR_DEVICE_MANUFACTURER, DEFAULT_MANUFACTURER)}"
+ + f" {data.get(ATTR_MODEL, Model.NOT_KNOWN).name}"
+ + f" ({data.get(ATTR_DEVICE_NAME, DEFAULT_NAME)})",
+ }
+
+ if "needUpdate" not in response or response["needUpdate"] != 1:
+ data[ATTR_UPDATE_FIRMWARE] = _rom_info
+
+ return
+
+ try:
+ data[ATTR_UPDATE_FIRMWARE] = _rom_info | {
+ ATTR_UPDATE_LATEST_VERSION: response["version"],
+ ATTR_UPDATE_DOWNLOAD_URL: response["downloadUrl"],
+ ATTR_UPDATE_RELEASE_URL: response["changelogUrl"],
+ ATTR_UPDATE_FILE_SIZE: response["fileSize"],
+ ATTR_UPDATE_FILE_HASH: response["fullHash"],
+ }
+ except KeyError:
+ pass
+
async def _async_prepare_mode(self, data: dict) -> None:
"""Prepare mode.
@@ -518,9 +568,9 @@ async def _async_prepare_wifi(self, data: dict) -> None:
length: int = 0
+ # Support only 5G , 2.4G, 5G Game and Guest
for wifi in response["info"]:
- # Support only 5G , 2.4G and 5G Game
- if "ifname" not in wifi or wifi["ifname"] not in ["wl0", "wl1", "wl2"]:
+ if "ifname" not in wifi:
continue
try:
@@ -530,7 +580,10 @@ async def _async_prepare_wifi(self, data: dict) -> None:
if "status" in wifi:
data[adapter.phrase] = int(wifi["status"]) > 0 # type: ignore
- length += 1
+
+ # Guest network is not an adapter
+ if adapter != IfName.WL14:
+ length += 1
if "channelInfo" in wifi and "channel" in wifi["channelInfo"]:
data[f"{adapter.phrase}_channel"] = str( # type: ignore
@@ -692,7 +745,7 @@ async def _async_prepare_device_list(self, data: dict) -> None:
ATTR_TRACKER_ENTRY_ID
]
- self.add_device(device, action=action)
+ self.add_device(device, action=action, integrations=integrations)
if len(add_to) == 0:
return
@@ -705,7 +758,9 @@ async def _async_prepare_device_list(self, data: dict) -> None:
integrations[_ip][UPDATER].reset_counter(True)
for device in devices.values():
- integrations[_ip][UPDATER].add_device(device[0], True, device[1])
+ integrations[_ip][UPDATER].add_device(
+ device[0], True, device[1], integrations
+ )
async def _async_prepare_device_restore(self, data: dict) -> None:
"""Restore devices
@@ -761,12 +816,14 @@ def add_device(
device: dict,
is_from_parent: bool = False,
action: DeviceAction = DeviceAction.ADD,
+ integrations: dict[str, Any] | None = None,
) -> None:
"""Prepare device.
:param device: dict
:param is_from_parent: bool: The call came from a third party integration
:param action: DeviceAction: Device action
+ :param integrations: dict[str, Any]: Integrations list
"""
if ATTR_TRACKER_MAC not in device or (is_from_parent and self.is_force_load):
@@ -822,6 +879,13 @@ def add_device(
ATTR_TRACKER_LAST_ACTIVITY: datetime.now()
.replace(microsecond=0)
.isoformat(),
+ ATTR_TRACKER_OPTIONAL_MAC: integrations[ip_attr["ip"]][UPDATER].data.get(
+ ATTR_DEVICE_MAC_ADDRESS, None
+ )
+ if integrations is not None
+ and ip_attr is not None
+ and ip_attr["ip"] in integrations
+ else None,
}
if not is_from_parent and action == DeviceAction.MOVE:
@@ -1018,6 +1082,9 @@ async def _async_save_devices(self) -> None:
async def _async_load_manufacturers(self) -> None:
"""Async load _manufacturers"""
+ if len(self._manufacturers) > 0:
+ return
+
self._manufacturers = await self.hass.async_add_executor_job(
json.load_json,
f"{os.path.dirname(os.path.abspath(__file__))}/manufacturers.json",
diff --git a/hacs.json b/hacs.json
index 874cb29..cf01ee3 100644
--- a/hacs.json
+++ b/hacs.json
@@ -11,5 +11,5 @@
"sensor",
"switch"
],
- "homeassistant": "2022.3.0"
+ "homeassistant": "2022.4.0"
}