mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 11:15:48 +02:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 242fe82232 | |||
| 4c7e92a355 | |||
| d03d06fb90 | |||
| 1e0c851b71 | |||
| dcec29dbbf | |||
| 1daff77591 | |||
| 7e3fc18c8c | |||
| b6cc5499aa | |||
| 11920b82fe | |||
| 2649504dfb | |||
| 90e8ed0210 | |||
| fd049891ff | |||
| e1b2de20d9 | |||
| ab52d02da9 | |||
| 98be2356ca | |||
| 9eddc58754 | |||
| e709189dab | |||
| b2ca7f4eef | |||
| 0a14e93cb3 | |||
| 352e89812f | |||
| 2f1730ed51 | |||
| 110ba33e43 | |||
| 9d96626e93 | |||
| 26df3f1966 | |||
| dc4f4ad5fe | |||
| 523f5dac48 | |||
| 19aecf2f1c | |||
| 6170156408 |
@@ -1,42 +0,0 @@
|
||||
name: Set up uv and managed Python
|
||||
description: >-
|
||||
Pins uv (avoids the raw.githubusercontent.com manifest fetch on cache miss)
|
||||
and proactively installs the requested Python so cached venvs created with
|
||||
`uv venv` resolve their interpreter symlinks in jobs that only restore the
|
||||
venv. setup-uv alone only sets UV_PYTHON, it does not actually fetch the
|
||||
interpreter until uv first uses it, so jobs that just activate the venv
|
||||
blow up with broken symlinks on cache hit.
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: The Python version uv should install and use.
|
||||
required: true
|
||||
uv-version:
|
||||
description: The uv version setup-uv should install.
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
python-version:
|
||||
description: The Python version uv reports as installed.
|
||||
value: ${{ steps.uv.outputs.python-version }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set up uv
|
||||
id: uv
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: ${{ inputs.uv-version }}
|
||||
python-version: ${{ inputs.python-version }}
|
||||
# Persist astral's managed Python across jobs so 'uv venv' below is
|
||||
# fast on the second job onwards.
|
||||
cache-python: true
|
||||
# Lint-only and codegen jobs touch no Python deps, so the post-step
|
||||
# cache save would otherwise abort the job.
|
||||
ignore-nothing-to-cache: true
|
||||
- name: Install Python interpreter
|
||||
shell: bash
|
||||
env:
|
||||
PYTHON_VERSION: ${{ inputs.python-version }}
|
||||
run: uv python install "${PYTHON_VERSION}"
|
||||
+47
-45
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 3
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.6"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
@@ -89,8 +89,6 @@ jobs:
|
||||
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
|
||||
postgresql_groups: ${{ steps.info.outputs.postgresql_groups }}
|
||||
python_versions: ${{ steps.info.outputs.python_versions }}
|
||||
default_python: ${{ steps.info.outputs.default_python }}
|
||||
uv_version: ${{ steps.info.outputs.uv_version }}
|
||||
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
|
||||
test_group_count: ${{ steps.info.outputs.test_group_count }}
|
||||
test_groups: ${{ steps.info.outputs.test_groups }}
|
||||
@@ -237,11 +235,6 @@ jobs:
|
||||
echo "postgresql_groups=${postgresql_groups}" >> $GITHUB_OUTPUT
|
||||
echo "python_versions: ${all_python_versions}"
|
||||
echo "python_versions=${all_python_versions}" >> $GITHUB_OUTPUT
|
||||
echo "default_python: ${default_python}"
|
||||
echo "default_python=${default_python}" >> $GITHUB_OUTPUT
|
||||
uv_version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)
|
||||
echo "uv_version: ${uv_version}"
|
||||
echo "uv_version=${uv_version}" >> $GITHUB_OUTPUT
|
||||
echo "test_full_suite: ${test_full_suite}"
|
||||
echo "test_full_suite=${test_full_suite}" >> $GITHUB_OUTPUT
|
||||
echo "integrations_glob: ${integrations_glob}"
|
||||
@@ -351,12 +344,12 @@ jobs:
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up uv and Python ${{ matrix.python-version }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -404,13 +397,21 @@ jobs:
|
||||
libudev-dev
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
execute_install_scripts: true
|
||||
- name: Read uv version from requirements.txt
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: read-uv-version
|
||||
run: |
|
||||
echo "version=$(grep '^uv==' requirements.txt | cut -d'=' -f3)" >> "$GITHUB_OUTPUT"
|
||||
- name: Set up uv
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: ${{ steps.read-uv-version.outputs.version }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: create-venv
|
||||
env:
|
||||
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
|
||||
run: |
|
||||
uv venv venv --python "${PYTHON_VERSION}"
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
uv pip install -r requirements.txt
|
||||
@@ -418,6 +419,7 @@ jobs:
|
||||
uv pip install -e . --config-settings editable_mode=compat
|
||||
- name: Dump pip freeze
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
uv pip freeze >> pip_freeze.txt
|
||||
@@ -478,10 +480,10 @@ jobs:
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -515,10 +517,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -551,10 +553,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Run gen_copilot_instructions.py
|
||||
run: |
|
||||
python -m script.gen_copilot_instructions validate
|
||||
@@ -606,10 +608,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -657,10 +659,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -710,10 +712,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -761,10 +763,10 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Generate partial mypy restore key
|
||||
id: generate-mypy-key
|
||||
run: |
|
||||
@@ -838,10 +840,10 @@ jobs:
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ needs.info.outputs.default_python }}
|
||||
python-version-file: ".python-version"
|
||||
check-latest: true
|
||||
- name: Restore full Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -903,10 +905,10 @@ jobs:
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1045,10 +1047,10 @@ jobs:
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1201,10 +1203,10 @@ jobs:
|
||||
version: ${{ env.APT_CACHE_VERSION }}
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -1369,10 +1371,10 @@ jobs:
|
||||
execute_install_scripts: true
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: ./.github/actions/setup-uv-python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
uv-version: ${{ needs.info.outputs.uv_version }}
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
|
||||
Generated
+2
-2
@@ -2054,8 +2054,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/yamaha_musiccast/ @vigonotion @micha91
|
||||
/homeassistant/components/yandex_transport/ @rishatik92 @devbis
|
||||
/tests/components/yandex_transport/ @rishatik92 @devbis
|
||||
/homeassistant/components/yardian/ @h3l1o5
|
||||
/tests/components/yardian/ @h3l1o5
|
||||
/homeassistant/components/yardian/ @aeon-matrix
|
||||
/tests/components/yardian/ @aeon-matrix
|
||||
/homeassistant/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015
|
||||
/homeassistant/components/yeelightsunflower/ @lindsaymarkward
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.3.0",
|
||||
"bluetooth-auto-recovery==1.6.4",
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.14",
|
||||
"dbus-fast==5.0.15",
|
||||
"habluetooth==6.7.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Assistant Hardware",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"dependencies": ["usb"],
|
||||
"dependencies": ["repairs", "usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Repairs for the Home Assistant Hardware integration."""
|
||||
|
||||
from homeassistant.components.repairs import RepairsFlow, RepairsFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
ISSUE_MULTI_PAN_MIGRATION = "multi_pan_migration"
|
||||
|
||||
|
||||
@callback
|
||||
def _multi_pan_issue_id(config_entry: ConfigEntry) -> str:
|
||||
"""Return the issue id for the multi-PAN migration issue of an entry."""
|
||||
return f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Create a repair issue to guide migration away from Multi-PAN."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=domain,
|
||||
issue_id=_multi_pan_issue_id(config_entry),
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": config_entry.title},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_delete_multi_pan_migration_issue(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Delete the multi-PAN migration repair issue for this entry."""
|
||||
ir.async_delete_issue(hass, domain, _multi_pan_issue_id(config_entry))
|
||||
|
||||
|
||||
class MultiPanMigrationRepairFlow(RepairsFlow):
|
||||
"""Reuse the multi-PAN options flow uninstall steps as a repair flow.
|
||||
|
||||
Subclass this together with the hardware-specific
|
||||
``MultiPanOptionsFlowHandler`` in each hardware integration's repairs
|
||||
module.
|
||||
|
||||
The repair flow runs in the repairs flow manager where ``self.handler``
|
||||
is the integration domain rather than the hardware config entry id, so
|
||||
the ``config_entry`` accessor of ``OptionsFlow`` must be overridden.
|
||||
"""
|
||||
|
||||
_repair_config_entry: ConfigEntry
|
||||
|
||||
@property
|
||||
def config_entry(self) -> ConfigEntry:
|
||||
"""Return the hardware config entry to migrate."""
|
||||
return self._repair_config_entry
|
||||
|
||||
async def _async_step_start_migration(self) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step of the migration flow.
|
||||
|
||||
The repair flow's init data is the issue context, not user form input,
|
||||
so pass None to render the uninstall confirmation form.
|
||||
"""
|
||||
return await self.async_step_uninstall_addon() # type: ignore[attr-defined, no-any-return]
|
||||
@@ -6,6 +6,8 @@ import dataclasses
|
||||
import logging
|
||||
from typing import Any, Protocol
|
||||
|
||||
from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
@@ -25,6 +27,7 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.integration_platform import (
|
||||
async_process_integration_platforms,
|
||||
@@ -37,15 +40,18 @@ from homeassistant.helpers.selector import (
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from .const import LOGGER, SILABS_FLASHER_ADDON_SLUG, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .const import DOMAIN, LOGGER, SILABS_MULTIPROTOCOL_ADDON_SLUG
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
WaitingAddonManager,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_MULTIPROTOCOL_ADDON_MANAGER = "silabs_multiprotocol_addon_manager"
|
||||
DATA_FLASHER_ADDON_MANAGER = "silabs_flasher"
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
CONF_ADDON_AUTOFLASH_FW = "autoflash_firmware"
|
||||
CONF_ADDON_DEVICE = "device"
|
||||
@@ -71,53 +77,6 @@ async def get_multiprotocol_addon_manager(
|
||||
return manager
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state is AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class MultiprotocolAddonManager(WaitingAddonManager):
|
||||
"""Silicon Labs Multiprotocol add-on manager."""
|
||||
|
||||
@@ -265,18 +224,6 @@ class MultipanProtocol(Protocol):
|
||||
"""
|
||||
|
||||
|
||||
@singleton(DATA_FLASHER_ADDON_MANAGER)
|
||||
@callback
|
||||
def get_flasher_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||
"""Get the flasher add-on manager."""
|
||||
return WaitingAddonManager(
|
||||
hass,
|
||||
LOGGER,
|
||||
"Silicon Labs Flasher",
|
||||
SILABS_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SerialPortSettings:
|
||||
"""Serial port settings."""
|
||||
@@ -339,6 +286,19 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
def _zha_name(self) -> str:
|
||||
"""Return the ZHA name."""
|
||||
|
||||
@abstractmethod
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
|
||||
@abstractmethod
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier (e.g. 'yellow_zigbee_ncp')."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
|
||||
@property
|
||||
def flow_manager(self) -> OptionsFlowManager:
|
||||
"""Return the correct flow manager."""
|
||||
@@ -686,61 +646,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
async def async_step_firmware_revert(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Install the flasher addon, if necessary."""
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
if addon_info.state is AddonState.NOT_INSTALLED:
|
||||
return await self.async_step_install_flasher_addon()
|
||||
|
||||
if addon_info.state is AddonState.NOT_RUNNING:
|
||||
return await self.async_step_configure_flasher_addon()
|
||||
|
||||
# If the addon is already installed and running, fail
|
||||
return self.async_abort(
|
||||
reason="addon_already_running",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
)
|
||||
|
||||
async def async_step_install_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show progress dialog for installing flasher addon."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
|
||||
_LOGGER.debug("Flasher addon state: %s", addon_info)
|
||||
|
||||
if not self.install_task:
|
||||
self.install_task = self.hass.async_create_task(
|
||||
flasher_manager.async_install_addon_waiting(),
|
||||
"SiLabs Flasher addon install",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_flasher_addon",
|
||||
progress_action="install_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.install_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="install_failed")
|
||||
finally:
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="configure_flasher_addon")
|
||||
|
||||
async def async_step_configure_flasher_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform initial backup and reconfigure ZHA."""
|
||||
"""Initiate ZHA backup and start multiprotocol addon uninstall."""
|
||||
# pylint: disable=home-assistant-component-root-import
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
@@ -782,17 +688,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
_LOGGER.exception("Unexpected exception during ZHA migration")
|
||||
raise AbortFlow("zha_migration_failed") from err
|
||||
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
addon_info = await self._async_get_addon_info(flasher_manager)
|
||||
new_addon_config = {
|
||||
**addon_info.options,
|
||||
"device": new_settings.device,
|
||||
"flow_control": new_settings.flow_control,
|
||||
}
|
||||
|
||||
_LOGGER.debug("Reconfiguring flasher addon with %s", new_addon_config)
|
||||
await self._async_set_addon_config(new_addon_config, flasher_manager)
|
||||
|
||||
return await self.async_step_uninstall_multiprotocol_addon()
|
||||
|
||||
async def async_step_uninstall_multiprotocol_addon(
|
||||
@@ -821,62 +716,93 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
finally:
|
||||
self.stop_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="start_flasher_addon")
|
||||
return self.async_show_progress_done(next_step_id="install_zigbee_firmware")
|
||||
|
||||
async def async_step_start_flasher_addon(
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Start Silicon Labs Flasher add-on."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Flash Zigbee firmware directly onto the radio."""
|
||||
if not self.install_task:
|
||||
|
||||
if not self.start_task:
|
||||
async def _flash_firmware() -> None:
|
||||
serial_port_settings = await self._async_serial_port_settings()
|
||||
device = serial_port_settings.device
|
||||
|
||||
async def start_and_wait_until_done() -> None:
|
||||
await flasher_manager.async_start_addon_waiting()
|
||||
# Now that the addon is running, wait for it to finish
|
||||
await flasher_manager.async_wait_until_addon_state(
|
||||
AddonState.NOT_RUNNING
|
||||
)
|
||||
# For the duration of firmware flashing, hint to other integrations
|
||||
# (i.e. ZHA) that the hardware is in use and should not be accessed.
|
||||
async with async_firmware_flashing_context(self.hass, device, DOMAIN):
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = FirmwareUpdateClient(self._firmware_update_url(), session)
|
||||
|
||||
self.start_task = self.hass.async_create_task(
|
||||
start_and_wait_until_done(), eager_start=False
|
||||
try:
|
||||
manifest = await client.async_update_data()
|
||||
fw_manifest = next(
|
||||
fw
|
||||
for fw in manifest.firmwares
|
||||
if fw.filename.startswith(self._zigbee_firmware_type())
|
||||
)
|
||||
fw_data = await client.async_fetch_firmware(fw_manifest)
|
||||
except (
|
||||
StopIteration,
|
||||
TimeoutError,
|
||||
ClientError,
|
||||
ManifestMissing,
|
||||
ValueError,
|
||||
) as err:
|
||||
raise HomeAssistantError(
|
||||
"Failed to fetch Zigbee firmware"
|
||||
) from err
|
||||
|
||||
await async_flash_silabs_firmware(
|
||||
hass=self.hass,
|
||||
device=device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=ApplicationType.EZSP,
|
||||
progress_callback=lambda offset, total: (
|
||||
self.async_update_progress(offset / total)
|
||||
),
|
||||
)
|
||||
|
||||
self.install_task = self.hass.async_create_task(
|
||||
_flash_firmware(),
|
||||
"Flash Zigbee firmware",
|
||||
eager_start=False,
|
||||
)
|
||||
|
||||
if not self.start_task.done():
|
||||
if not self.install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="start_flasher_addon",
|
||||
progress_action="start_flasher_addon",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
progress_task=self.start_task,
|
||||
step_id="install_zigbee_firmware",
|
||||
progress_action="install_zigbee_firmware",
|
||||
description_placeholders={
|
||||
"hardware_name": self._hardware_name(),
|
||||
},
|
||||
progress_task=self.install_task,
|
||||
)
|
||||
|
||||
try:
|
||||
await self.start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
return self.async_show_progress_done(next_step_id="flasher_failed")
|
||||
await self.install_task
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Failed to flash Zigbee firmware: %s", err)
|
||||
return self.async_show_progress_done(next_step_id="firmware_flash_failed")
|
||||
finally:
|
||||
self.start_task = None
|
||||
self.install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="flashing_complete")
|
||||
|
||||
async def async_step_flasher_failed(
|
||||
async def async_step_firmware_flash_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Flasher add-on start failed."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
"""Firmware flashing failed."""
|
||||
return self.async_abort(
|
||||
reason="addon_start_failed",
|
||||
description_placeholders={"addon_name": flasher_manager.addon_name},
|
||||
reason="fw_install_failed",
|
||||
description_placeholders={"firmware_name": "Zigbee"},
|
||||
)
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Finish flashing and update the config entry."""
|
||||
flasher_manager = get_flasher_addon_manager(self.hass)
|
||||
await flasher_manager.async_uninstall_addon_waiting()
|
||||
|
||||
# Finish ZHA migration if needed
|
||||
if self._zha_migration_mgr:
|
||||
try:
|
||||
|
||||
@@ -102,7 +102,9 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "Please wait while the {addon_name} app installation finishes. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds."
|
||||
"install_zigbee_firmware": "Please wait while Zigbee-only firmware is installed on your {hardware_name}. This can take several minutes.",
|
||||
"start_addon": "Please wait while the {addon_name} app start completes. This may take some seconds.",
|
||||
"uninstall_multiprotocol_addon": "Please wait while the {addon_name} app is uninstalled."
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -37,13 +37,59 @@ from .const import (
|
||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
from .helpers import async_firmware_update_context
|
||||
from .silabs_multiprotocol_addon import (
|
||||
WaitingAddonManager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ADDON_STATE_POLL_INTERVAL = 3
|
||||
ADDON_INFO_POLL_TIMEOUT = 15 * 60
|
||||
|
||||
|
||||
class WaitingAddonManager(AddonManager):
|
||||
"""Addon manager which supports waiting operations for managing an addon."""
|
||||
|
||||
async def async_wait_until_addon_state(self, *states: AddonState) -> None:
|
||||
"""Poll an addon's info until it is in a specific state."""
|
||||
async with asyncio.timeout(ADDON_INFO_POLL_TIMEOUT):
|
||||
while True:
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
_LOGGER.debug("Waiting for addon to be in state %s: %s", states, info)
|
||||
|
||||
if info is not None and info.state in states:
|
||||
break
|
||||
|
||||
await asyncio.sleep(ADDON_STATE_POLL_INTERVAL)
|
||||
|
||||
async def async_start_addon_waiting(self) -> None:
|
||||
"""Start an add-on."""
|
||||
await self.async_schedule_start_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.RUNNING)
|
||||
|
||||
async def async_install_addon_waiting(self) -> None:
|
||||
"""Install an add-on."""
|
||||
await self.async_schedule_install_addon()
|
||||
await self.async_wait_until_addon_state(
|
||||
AddonState.RUNNING,
|
||||
AddonState.NOT_RUNNING,
|
||||
)
|
||||
|
||||
async def async_uninstall_addon_waiting(self) -> None:
|
||||
"""Uninstall an add-on."""
|
||||
try:
|
||||
info = await self.async_get_addon_info()
|
||||
except AddonError:
|
||||
info = None
|
||||
|
||||
# Do not try to uninstall an addon if it is already uninstalled
|
||||
if info is not None and info.state == AddonState.NOT_INSTALLED:
|
||||
return
|
||||
|
||||
await self.async_uninstall_addon()
|
||||
await self.async_wait_until_addon_state(AddonState.NOT_INSTALLED)
|
||||
|
||||
|
||||
class ApplicationType(StrEnum):
|
||||
"""Application type running on a device."""
|
||||
@@ -279,6 +325,11 @@ async def guess_hardware_owners(
|
||||
assert otbr_addon_fw_info is not None
|
||||
device_guesses[otbr_path].append(otbr_addon_fw_info)
|
||||
|
||||
# Lazy import to avoid circular dependency
|
||||
from .silabs_multiprotocol_addon import ( # noqa: PLC0415
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
|
||||
multipan_addon_manager = await get_multiprotocol_addon_manager(hass)
|
||||
|
||||
try:
|
||||
|
||||
@@ -7,6 +7,13 @@ import os.path
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
@@ -92,6 +99,16 @@ async def async_setup_entry(
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
try:
|
||||
uses_multi_pan = await multi_pan_addon_using_device(hass, device_path)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if uses_multi_pan:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
# Create and store the firmware update coordinator in runtime_data
|
||||
session = async_get_clientsession(hass)
|
||||
coordinator = FirmwareUpdateCoordinator(
|
||||
|
||||
@@ -248,6 +248,19 @@ class HomeAssistantSkyConnectMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return self._hw_variant.full_name
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "skyconnect_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return Zbt1Flasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Repairs for the Home Assistant SkyConnect integration."""
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import (
|
||||
ConfirmRepairFlow,
|
||||
RepairsFlow,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class SkyConnectMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantSkyConnectMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant SkyConnect."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantSkyConnectMultiPanOptionsFlowHandler.__init__(self, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_init( # type: ignore[override]
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a SkyConnect repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return SkyConnectMultiPanMigrationRepairFlow(entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -106,6 +106,37 @@
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -130,8 +161,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -7,8 +7,13 @@ from homeassistant.components.hassio import HassioNotReadyError, get_os_info
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
check_multi_pan_addon,
|
||||
multi_pan_addon_using_device,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
@@ -27,6 +32,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
@@ -77,6 +83,16 @@ async def async_setup_entry(
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
try:
|
||||
multipan_using_device = await multi_pan_addon_using_device(hass, RADIO_DEVICE)
|
||||
except HomeAssistantError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if multipan_using_device:
|
||||
async_create_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
else:
|
||||
async_delete_multi_pan_migration_issue(hass, DOMAIN, entry)
|
||||
|
||||
if firmware is ApplicationType.EZSP:
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
|
||||
@@ -319,6 +319,19 @@ class HomeAssistantYellowMultiPanOptionsFlowHandler(
|
||||
"""Return the name of the hardware."""
|
||||
return BOARD_NAME
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "yellow_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return YellowFlasher # type: ignore[no-any-return]
|
||||
|
||||
async def async_step_flashing_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Repairs for the Home Assistant Yellow integration."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
MultiPanMigrationRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import (
|
||||
ConfirmRepairFlow,
|
||||
RepairsFlow,
|
||||
RepairsFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
|
||||
|
||||
class YellowMultiPanMigrationRepairFlow(
|
||||
MultiPanMigrationRepairFlow, HomeAssistantYellowMultiPanOptionsFlowHandler
|
||||
):
|
||||
"""Multi-PAN migration repair flow for Home Assistant Yellow."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the repair flow."""
|
||||
HomeAssistantYellowMultiPanOptionsFlowHandler.__init__(self, hass, config_entry)
|
||||
self._repair_config_entry = config_entry
|
||||
|
||||
async def async_step_main_menu( # type: ignore[override]
|
||||
self, _: None = None
|
||||
) -> RepairsFlowResult:
|
||||
"""Jump straight into the uninstall step."""
|
||||
return await self._async_step_start_migration()
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create a fix flow for a Yellow repair issue."""
|
||||
if issue_id.startswith(ISSUE_MULTI_PAN_MIGRATION) and data is not None:
|
||||
entry_id = cast(str, data["entry_id"])
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id)) is not None:
|
||||
return YellowMultiPanMigrationRepairFlow(hass, entry)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
@@ -11,6 +11,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"multi_pan_migration": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
"addon_info_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_info_failed%]",
|
||||
"addon_install_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_install_failed%]",
|
||||
"addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]",
|
||||
"addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]",
|
||||
"fw_install_failed": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::fw_install_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"uninstall_addon": {
|
||||
"data": {
|
||||
"disable_multi_pan": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::data::disable_multi_pan%]"
|
||||
},
|
||||
"description": "Multiprotocol support for the IEEE 802.15.4 radio in your {hardware_name} has been deprecated. Migrate the radio back to Zigbee-only firmware to keep it working in the future.\n\nDisabling multiprotocol support will disable Thread support provided by the {hardware_name}. Your Thread devices will continue working only if you have another Thread border router nearby.\n\nIt will take a few minutes to install the Zigbee firmware and restore a backup.",
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::uninstall_addon::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Multiprotocol support is deprecated"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"addon_already_running": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_already_running%]",
|
||||
@@ -37,8 +68,10 @@
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"install_zigbee_firmware": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_zigbee_firmware%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]",
|
||||
"uninstall_multiprotocol_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::uninstall_multiprotocol_addon%]"
|
||||
},
|
||||
"step": {
|
||||
"addon_installed_other_device": {
|
||||
|
||||
@@ -111,8 +111,11 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, OpowerData]]):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except CannotConnect as err:
|
||||
_LOGGER.error("Error during login: %s", err)
|
||||
# pylint: disable-next=home-assistant-exception-not-translated
|
||||
raise UpdateFailed(f"Error during login: {err}") from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="login_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
try:
|
||||
accounts = await self.api.async_get_accounts()
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"login_error": {
|
||||
"message": "Error during login: {error}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"return_to_grid_migration": {
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those into separate export statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Energy exported to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue.",
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/qingping",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["qingping-ble==1.1.4"]
|
||||
"requirements": ["qingping-ble==1.1.5"]
|
||||
}
|
||||
|
||||
@@ -217,9 +217,9 @@
|
||||
"home": "[%key:common::entity::button::home::name%]",
|
||||
"insert": "Insert",
|
||||
"left": "[%key:common::entity::button::left::name%]",
|
||||
"lights_kbd_down": "Keyboasrd backlight brightness down",
|
||||
"lights_kbd_down": "Keyboard backlight brightness down",
|
||||
"lights_kbd_toggle": "Toggle keyboard backlight",
|
||||
"lights_kbd_up": "Keyboard backlight brighness up",
|
||||
"lights_kbd_up": "Keyboard backlight brightness up",
|
||||
"lights_mon_down": "Display brightness down",
|
||||
"lights_mon_up": "Display brightness up",
|
||||
"numpad_0": "NumPad 0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "yardian",
|
||||
"name": "Yardian",
|
||||
"codeowners": ["@h3l1o5"],
|
||||
"codeowners": ["@aeon-matrix"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yardian",
|
||||
"integration_type": "device",
|
||||
|
||||
@@ -30,7 +30,7 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
cryptography==48.0.0
|
||||
dbus-fast==5.0.14
|
||||
dbus-fast==5.0.15
|
||||
file-read-backwards==2.0.0
|
||||
fnv-hash-fast==2.0.3
|
||||
go2rtc-client==0.4.0
|
||||
|
||||
Generated
+2
-2
@@ -797,7 +797,7 @@ datadog==0.52.0
|
||||
datapoint==0.12.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==5.0.14
|
||||
dbus-fast==5.0.15
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.8.17
|
||||
@@ -2839,7 +2839,7 @@ qbittorrent-api==2026.5.1
|
||||
qbusmqttapi==1.5.0
|
||||
|
||||
# homeassistant.components.qingping
|
||||
qingping-ble==1.1.4
|
||||
qingping-ble==1.1.5
|
||||
|
||||
# homeassistant.components.qnap
|
||||
qnapstats==0.4.0
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
@@ -99,6 +100,7 @@ async def test_setup_and_stop(
|
||||
assert len(mock_bleak_scanner_start.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux BlueZ scanner")
|
||||
@pytest.mark.parametrize(
|
||||
"options",
|
||||
[{CONF_MODE: "passive"}, {CONF_PASSIVE: True}],
|
||||
@@ -161,6 +163,7 @@ async def test_setup_and_stop_passive(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Requires Linux BlueZ scanner")
|
||||
async def test_setup_and_stop_old_bluez(
|
||||
hass: HomeAssistant,
|
||||
mock_bleak_scanner_start: MagicMock,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Tests for the homeassistant_hardware repairs helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
async_create_multi_pan_migration_issue,
|
||||
async_delete_multi_pan_migration_issue,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_DOMAIN = "test_hardware"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ignore_translations_for_mock_domains() -> str:
|
||||
"""Ignore translation check for the fake test_hardware domain."""
|
||||
return TEST_DOMAIN
|
||||
|
||||
|
||||
async def test_create_and_delete_multi_pan_migration_issue(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the helpers create and delete the migration issue per entry."""
|
||||
entry = MockConfigEntry(domain=TEST_DOMAIN, title="Test HW", data={})
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
async_create_multi_pan_migration_issue(hass, TEST_DOMAIN, entry)
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{entry.entry_id}"
|
||||
issue = issue_registry.async_get_issue(domain=TEST_DOMAIN, issue_id=issue_id)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {"hardware_name": "Test HW"}
|
||||
assert issue.data == {"entry_id": entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
assert issue.severity is ir.IssueSeverity.WARNING
|
||||
|
||||
async_delete_multi_pan_migration_issue(hass, TEST_DOMAIN, entry)
|
||||
assert issue_registry.async_get_issue(domain=TEST_DOMAIN, issue_id=issue_id) is None
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import AddonsOptions
|
||||
from aiohttp import ClientError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonInfo, AddonState, HassIO
|
||||
@@ -96,6 +97,19 @@ class FakeOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler):
|
||||
"""Return the name of the hardware."""
|
||||
return "Test"
|
||||
|
||||
def _firmware_update_url(self) -> str:
|
||||
"""Return the firmware update manifest URL."""
|
||||
return "https://example.com/firmware"
|
||||
|
||||
def _zigbee_firmware_type(self) -> str:
|
||||
"""Return the zigbee firmware type identifier."""
|
||||
return "test_zigbee_ncp"
|
||||
|
||||
@property
|
||||
def _flasher_cls(self) -> type:
|
||||
"""Return the hardware-specific flasher class."""
|
||||
return Mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def config_flow_handler(
|
||||
@@ -116,6 +130,31 @@ def options_flow_poll_addon_state() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_firmware_client() -> Generator[tuple[AsyncMock, AsyncMock]]:
|
||||
"""Fixture to mock FirmwareUpdateClient and async_flash_silabs_firmware."""
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "test_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_flash,
|
||||
):
|
||||
yield mock_fw_client, mock_flash
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def hassio_integration(hass: HomeAssistant) -> Generator[None]:
|
||||
"""Fixture to mock the `hassio` integration."""
|
||||
@@ -172,7 +211,7 @@ def get_suggested(schema, key):
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.ADDON_STATE_POLL_INTERVAL",
|
||||
"homeassistant.components.homeassistant_hardware.util.ADDON_STATE_POLL_INTERVAL",
|
||||
0,
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
@@ -631,12 +670,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
mock_firmware_client: tuple[AsyncMock, AsyncMock],
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
|
||||
@@ -673,21 +710,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Make sure the flasher addon is installed
|
||||
addon_store_info.return_value.installed = False
|
||||
addon_store_info.return_Value.available = True
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_flasher_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
@@ -697,12 +723,10 @@ async def test_option_flow_addon_installed_same_device_uninstall(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_called_once_with("core_silabs_flasher")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -724,11 +748,6 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
|
||||
@@ -762,19 +781,15 @@ async def test_option_flow_addon_installed_same_device_do_not_uninstall_multi_pa
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_already_running_failure(
|
||||
async def test_option_flow_firmware_flash_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon but with the flasher addon running."""
|
||||
"""Test uninstalling the multi pan addon, case where firmware flash fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -798,162 +813,60 @@ async def test_option_flow_flasher_already_running_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# The flasher addon is already installed and running, this is bad
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_info.return_value.state = "started"
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "test_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_already_running"
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=HomeAssistantError("Flash failed"),
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
async def test_option_flow_addon_installed_same_device_flasher_already_installed(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon."""
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Test HW",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "addon_menu"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "uninstall_addon"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_store_info.return_value.available = True
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
|
||||
addon_store_info.return_value.installed = True
|
||||
addon_store_info.return_value.available = True
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_not_called()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "fw_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_install_failure(
|
||||
async def test_option_flow_zigbee_firmware_fetch_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where flasher addon fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
# Setup the config entry
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=TEST_DOMAIN,
|
||||
options={},
|
||||
title="Test HW",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
zha_config_entry = MockConfigEntry(
|
||||
data={
|
||||
"device": {"path": "socket://core-silabs-multiprotocol:9999"},
|
||||
"radio_type": "ezsp",
|
||||
},
|
||||
domain=ZHA_DOMAIN,
|
||||
options={},
|
||||
title="Test Multiprotocol",
|
||||
)
|
||||
zha_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "addon_menu"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "uninstall_addon"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
addon_store_info.return_value.installed = False
|
||||
addon_store_info.return_value.available = True
|
||||
install_addon.side_effect = [AddonError()]
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_flasher_addon"
|
||||
assert result["progress_action"] == "install_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
install_addon.assert_called_once_with("core_silabs_flasher")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
async def test_option_flow_flasher_addon_flash_failure(
|
||||
hass: HomeAssistant,
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test where flasher addon fails to flash Zigbee firmware."""
|
||||
"""Test where fetching Zigbee firmware fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -977,30 +890,39 @@ async def test_option_flow_flasher_addon_flash_failure(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], {silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.side_effect = ClientError("Network error")
|
||||
|
||||
start_addon.side_effect = SupervisorError("Boom")
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{silabs_multiprotocol_addon.CONF_DISABLE_MULTI_PAN: True},
|
||||
)
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
await hass.async_block_till_done()
|
||||
uninstall_addon.assert_called_once_with("core_silabs_multiprotocol")
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "addon_start_failed"
|
||||
assert result["description_placeholders"]["addon_name"] == "Silicon Labs Flasher"
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "fw_install_failed"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_translations_for_mock_domains", ["test"])
|
||||
@@ -1014,11 +936,6 @@ async def test_option_flow_uninstall_migration_initiate_failure(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration init fails."""
|
||||
@@ -1076,14 +993,11 @@ async def test_option_flow_uninstall_migration_finish_failure(
|
||||
addon_info,
|
||||
addon_store_info,
|
||||
addon_installed,
|
||||
install_addon,
|
||||
start_addon,
|
||||
stop_addon,
|
||||
uninstall_addon,
|
||||
set_addon_options,
|
||||
options_flow_poll_addon_state,
|
||||
mock_firmware_client: tuple[AsyncMock, AsyncMock],
|
||||
) -> None:
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration init fails."""
|
||||
"""Test uninstalling the multi pan addon, case where ZHA migration finish fails."""
|
||||
|
||||
addon_info.return_value.options["device"] = "/dev/ttyTEST123"
|
||||
|
||||
@@ -1127,9 +1041,8 @@ async def test_option_flow_uninstall_migration_finish_failure(
|
||||
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_flasher_addon"
|
||||
assert result["progress_action"] == "start_flasher_addon"
|
||||
assert result["description_placeholders"] == {"addon_name": "Silicon Labs Flasher"}
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ async def test_guess_hardware_owners_z2m(
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
@@ -290,7 +290,7 @@ async def test_guess_hardware_owners_otbr(hass: HomeAssistant) -> None:
|
||||
return_value=otbr_addon_fw_info,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
@@ -334,7 +334,7 @@ async def test_guess_hardware_owners_multipan(hass: HomeAssistant) -> None:
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.util.get_multiprotocol_addon_manager",
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=multipan_addon_manager,
|
||||
),
|
||||
patch(
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_flasher_addon_manager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
@@ -384,15 +383,11 @@ async def test_options_flow_multipan_uninstall(
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass))
|
||||
mock_flasher_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "skyconnect_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -400,8 +395,12 @@ async def test_options_flow_multipan_uninstall(
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -424,11 +423,15 @@ async def test_options_flow_multipan_uninstall(
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Finish the flow
|
||||
# Uninstall multiprotocol addon
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash zigbee firmware
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -26,6 +29,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -416,13 +420,14 @@ async def test_usb_device_reactivity(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Wait for a bit for the USB scan debouncer to cool off
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5))
|
||||
|
||||
# Unplug the stick
|
||||
# Unplug the stick before advancing time: the forced polling watcher rescans on
|
||||
# the time jump used to cool off the request debouncer, so the device must
|
||||
# already be gone or that scan would reload it as still present
|
||||
mock_exists.return_value = False
|
||||
|
||||
with patch_scanned_serial_ports(return_value=[]):
|
||||
# Wait for a bit for the USB scan debouncer to cool off
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(minutes=5))
|
||||
await async_request_scan(hass)
|
||||
|
||||
# The integration has reloaded and is now in a failed state
|
||||
@@ -550,3 +555,127 @@ async def test_bad_config_entry_fixing(hass: HomeAssistant) -> None:
|
||||
|
||||
untouched_bad_entry = hass.config_entries.async_get_entry(bad_entry.entry_id)
|
||||
assert untouched_bad_entry.minor_version == 3
|
||||
|
||||
|
||||
def _multi_pan_sky_connect_entry(firmware: str) -> MockConfigEntry:
|
||||
"""Return a SkyConnect config entry with the given firmware type."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0",
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": firmware,
|
||||
"firmware_version": None,
|
||||
},
|
||||
title="Home Assistant SkyConnect",
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_not_created_for_cpc(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test no repair issue is created for CPC firmware when the addon is not running."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.CPC.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_addon(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the repair issue is created when the multi-PAN addon is running."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.SPINEL.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {
|
||||
"hardware_name": "Home Assistant SkyConnect"
|
||||
}
|
||||
assert issue.data == {"entry_id": config_entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_deleted_for_ezsp(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is removed when not using multi-PAN."""
|
||||
config_entry = _multi_pan_sky_connect_entry(ApplicationType.EZSP.value)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": "Home Assistant SkyConnect"},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""Test the Home Assistant SkyConnect repairs flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
DOMAIN as HASSIO_DOMAIN,
|
||||
AddonInfo,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_sky_connect.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
start_repair_fix_flow,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
DEVICE = (
|
||||
"/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0"
|
||||
"_9e2adbd75b8beb119fe564a0f320645d-if00-port0"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor_client")
|
||||
async def test_multi_pan_migration_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair flow reverts the firmware with progress."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="some_unique_id",
|
||||
data={
|
||||
"description": "SkyConnect v1.0",
|
||||
"device": DEVICE,
|
||||
"vid": "10C4",
|
||||
"pid": "EA60",
|
||||
"serial_number": "3c0ed67c628beb11b1cd64a0f320645d",
|
||||
"manufacturer": "Nabu Casa",
|
||||
"product": "SkyConnect v1.0",
|
||||
"firmware": "cpc",
|
||||
"firmware_version": None,
|
||||
},
|
||||
title="Home Assistant SkyConnect",
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Multi-PAN addon is running and using the radio
|
||||
mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass))
|
||||
mock_multipan_manager.addon_name = "Silicon Labs Multiprotocol"
|
||||
mock_multipan_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": DEVICE},
|
||||
state=AddonState.RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "skyconnect_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_sky_connect.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
# Setting up the entry creates the migration issue
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# The repair flow jumps straight into the uninstall confirmation form
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
flow_id = result["flow_id"]
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Confirm the migration: uninstall the multiprotocol addon (progress)
|
||||
result = await process_repair_fix_flow(
|
||||
client, flow_id, json={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash the Zigbee firmware (progress)
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete, the flow finishes
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
# The firmware was reverted back to Zigbee
|
||||
assert config_entry.data["firmware"] == "ezsp"
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.homeassistant_hardware.firmware_config_flow import
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_flasher_addon_manager,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
@@ -550,15 +549,11 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_flasher_manager = Mock(spec_set=get_flasher_addon_manager(hass))
|
||||
mock_flasher_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={},
|
||||
state=AddonState.NOT_RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "yellow_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -566,8 +561,12 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_flasher_addon_manager",
|
||||
return_value=mock_flasher_manager,
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
@@ -596,11 +595,15 @@ async def test_options_flow_multipan_uninstall(hass: HomeAssistant) -> None:
|
||||
result["flow_id"], user_input={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
|
||||
# Finish the flow
|
||||
# Uninstall multiprotocol addon
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash zigbee firmware
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete
|
||||
result = await hass.config_entries.options.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import pytest
|
||||
|
||||
from homeassistant.components import zha
|
||||
from homeassistant.components.hassio import DOMAIN as HASSIO_DOMAIN, HassioNotReadyError
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
@@ -18,6 +21,7 @@ from homeassistant.components.usb import SerialDevice, async_scan_serial_ports
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
@@ -321,6 +325,157 @@ async def test_setup_entry_addon_info_fails(
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_not_created_for_cpc(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test no repair issue is created for CPC firmware when the addon is not running."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.CPC},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.check_multi_pan_addon",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_created_for_addon(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the repair issue is created when the multi-PAN addon is running."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.SPINEL},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
assert issue is not None
|
||||
assert issue.translation_key == ISSUE_MULTI_PAN_MIGRATION
|
||||
assert issue.translation_placeholders == {"hardware_name": "Home Assistant Yellow"}
|
||||
assert issue.data == {"entry_id": config_entry.entry_id}
|
||||
assert issue.is_fixable
|
||||
|
||||
|
||||
async def test_multi_pan_migration_issue_deleted_for_ezsp(
|
||||
hass: HomeAssistant,
|
||||
addon_store_info,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair issue is removed when not using multi-PAN."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.EZSP},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Pre-existing issue from a previous CPC run
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=ISSUE_MULTI_PAN_MIGRATION,
|
||||
translation_placeholders={"hardware_name": "Home Assistant Yellow"},
|
||||
data={"entry_id": config_entry.entry_id},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.onboarding.async_is_onboarded",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
issue_registry.async_get_issue(
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("start_version", "data", "migrated_data"),
|
||||
[
|
||||
@@ -379,6 +534,10 @@ async def test_migrate_entry(
|
||||
owners=[],
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.multi_pan_addon_using_device",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Test the Home Assistant Yellow repairs flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
DOMAIN as HASSIO_DOMAIN,
|
||||
AddonInfo,
|
||||
AddonState,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.repair_helpers import (
|
||||
ISSUE_MULTI_PAN_MIGRATION,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import (
|
||||
CONF_DISABLE_MULTI_PAN,
|
||||
get_multiprotocol_addon_manager,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.components.homeassistant_yellow.const import DOMAIN, RADIO_DEVICE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, MockModule, mock_integration
|
||||
from tests.components.repairs import (
|
||||
async_process_repairs_platforms,
|
||||
process_repair_fix_flow,
|
||||
start_repair_fix_flow,
|
||||
)
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_multi_pan_migration_repair_flow(
|
||||
hass: HomeAssistant,
|
||||
supervisor_client: AsyncMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test the multi-PAN migration repair flow reverts the firmware with progress."""
|
||||
mock_integration(hass, MockModule("hassio"))
|
||||
await async_setup_component(hass, HASSIO_DOMAIN, {})
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={"firmware": ApplicationType.CPC},
|
||||
domain=DOMAIN,
|
||||
options={},
|
||||
title="Home Assistant Yellow",
|
||||
version=1,
|
||||
minor_version=2,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
# Multi-PAN addon is running and using the radio
|
||||
mock_multipan_manager = Mock(spec_set=await get_multiprotocol_addon_manager(hass))
|
||||
mock_multipan_manager.addon_name = "Silicon Labs Multiprotocol"
|
||||
mock_multipan_manager.async_get_addon_info.return_value = AddonInfo(
|
||||
available=True,
|
||||
hostname=None,
|
||||
options={"device": RADIO_DEVICE},
|
||||
state=AddonState.RUNNING,
|
||||
update_available=False,
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
mock_fw_manifest = Mock()
|
||||
mock_fw_manifest.filename = "yellow_zigbee_ncp_7.4.4.0.gbl"
|
||||
mock_fw_client = AsyncMock()
|
||||
mock_fw_client.async_update_data.return_value = Mock(firmwares=[mock_fw_manifest])
|
||||
mock_fw_client.async_fetch_firmware.return_value = b"fake_firmware"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.get_os_info",
|
||||
return_value={"board": "yellow"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.config_flow.get_supervisor_client",
|
||||
return_value=supervisor_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_yellow.check_multi_pan_addon",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.get_multiprotocol_addon_manager",
|
||||
return_value=mock_multipan_manager,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_flash_silabs_firmware",
|
||||
new_callable=AsyncMock,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.async_firmware_flashing_context"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.FirmwareUpdateClient",
|
||||
return_value=mock_fw_client,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon.is_hassio",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
# Setting up the entry creates the migration issue
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
|
||||
issue_id = f"{ISSUE_MULTI_PAN_MIGRATION}_{config_entry.entry_id}"
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id) is not None
|
||||
|
||||
# The repair flow jumps straight into the uninstall confirmation form
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
flow_id = result["flow_id"]
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "uninstall_addon"
|
||||
|
||||
# Confirm the migration: uninstall the multiprotocol addon (progress)
|
||||
result = await process_repair_fix_flow(
|
||||
client, flow_id, json={CONF_DISABLE_MULTI_PAN: True}
|
||||
)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "uninstall_multiprotocol_addon"
|
||||
assert result["progress_action"] == "uninstall_multiprotocol_addon"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flash the Zigbee firmware (progress)
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "install_zigbee_firmware"
|
||||
assert result["progress_action"] == "install_zigbee_firmware"
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
# Flashing complete, the flow finishes
|
||||
result = await process_repair_fix_flow(client, flow_id)
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
# The firmware was reverted back to Zigbee
|
||||
assert config_entry.data["firmware"] == "ezsp"
|
||||
Reference in New Issue
Block a user