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" }