mirror of
https://github.com/home-assistant/core.git
synced 2025-11-23 09:46:54 +00:00
Compare commits
3 Commits
dev
...
shelly_ope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29593bb503 | ||
|
|
85b75d47d2 | ||
|
|
d16e0a9bd9 |
@@ -60,6 +60,7 @@ from .coordinator import (
|
||||
from .repairs import (
|
||||
async_manage_ble_scanner_firmware_unsupported_issue,
|
||||
async_manage_deprecated_firmware_issue,
|
||||
async_manage_open_wifi_ap_issue,
|
||||
async_manage_outbound_websocket_incorrectly_enabled_issue,
|
||||
)
|
||||
from .utils import (
|
||||
@@ -347,6 +348,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
hass,
|
||||
entry,
|
||||
)
|
||||
async_manage_open_wifi_ap_issue(hass, entry)
|
||||
remove_empty_sub_devices(hass, entry)
|
||||
elif (
|
||||
sleep_period is None
|
||||
|
||||
@@ -254,6 +254,7 @@ OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID = (
|
||||
"outbound_websocket_incorrectly_enabled_{unique}"
|
||||
)
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID = "deprecated_firmware_{unique}"
|
||||
OPEN_WIFI_AP_ISSUE_ID = "open_wifi_ap_{unique}"
|
||||
|
||||
|
||||
class DeprecatedFirmwareInfo(TypedDict):
|
||||
|
||||
@@ -22,6 +22,7 @@ from .const import (
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID,
|
||||
DEPRECATED_FIRMWARES,
|
||||
DOMAIN,
|
||||
OPEN_WIFI_AP_ISSUE_ID,
|
||||
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID,
|
||||
BLEScannerMode,
|
||||
)
|
||||
@@ -149,6 +150,45 @@ def async_manage_outbound_websocket_incorrectly_enabled_issue(
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_manage_open_wifi_ap_issue(
|
||||
hass: HomeAssistant,
|
||||
entry: ShellyConfigEntry,
|
||||
) -> None:
|
||||
"""Manage the open WiFi AP issue."""
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=entry.unique_id)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert entry.runtime_data.rpc is not None
|
||||
|
||||
device = entry.runtime_data.rpc.device
|
||||
|
||||
# Check if WiFi AP is enabled and has no password (open/unsecured)
|
||||
if (
|
||||
(wifi_config := device.config.get("wifi"))
|
||||
and (ap_config := wifi_config.get("ap"))
|
||||
and ap_config.get("enable")
|
||||
and not ap_config.get("pass")
|
||||
):
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=True,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="open_wifi_ap",
|
||||
translation_placeholders={
|
||||
"device_name": device.name,
|
||||
"ip_address": device.ip_address,
|
||||
},
|
||||
data={"entry_id": entry.entry_id},
|
||||
)
|
||||
return
|
||||
|
||||
ir.async_delete_issue(hass, DOMAIN, issue_id)
|
||||
|
||||
|
||||
class ShellyRpcRepairsFlow(RepairsFlow):
|
||||
"""Handler for an issue fixing flow."""
|
||||
|
||||
@@ -229,6 +269,49 @@ class DisableOutboundWebSocketFlow(ShellyRpcRepairsFlow):
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
|
||||
class DisableOpenWiFiApFlow(RepairsFlow):
|
||||
"""Handler for Disable Open WiFi AP flow."""
|
||||
|
||||
def __init__(self, device: RpcDevice, issue_id: str) -> None:
|
||||
"""Initialize."""
|
||||
self._device = device
|
||||
self.issue_id = issue_id
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
issue_registry = ir.async_get(self.hass)
|
||||
description_placeholders = None
|
||||
if issue := issue_registry.async_get_issue(DOMAIN, self.issue_id):
|
||||
description_placeholders = issue.translation_placeholders
|
||||
|
||||
return self.async_show_menu(
|
||||
menu_options=["confirm", "ignore"],
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
try:
|
||||
result = await self._device.wifi_setconfig(ap_enable=False)
|
||||
if result.get("restart_required"):
|
||||
await self._device.trigger_reboot()
|
||||
except (DeviceConnectionError, RpcCallError):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
return self.async_create_entry(title="", data={})
|
||||
|
||||
async def async_step_ignore(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the ignore step of a fix flow."""
|
||||
ir.async_ignore_issue(self.hass, DOMAIN, self.issue_id, True)
|
||||
return self.async_abort(reason="issue_ignored")
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant, issue_id: str, data: dict[str, str] | None
|
||||
) -> RepairsFlow:
|
||||
@@ -253,4 +336,7 @@ async def async_create_fix_flow(
|
||||
if "outbound_websocket_incorrectly_enabled" in issue_id:
|
||||
return DisableOutboundWebSocketFlow(device)
|
||||
|
||||
if "open_wifi_ap" in issue_id:
|
||||
return DisableOpenWiFiApFlow(device, issue_id)
|
||||
|
||||
return ConfirmRepairFlow()
|
||||
|
||||
@@ -664,6 +664,25 @@
|
||||
"description": "Shelly device {device_name} with IP address {ip_address} requires calibration. To calibrate the device, it must be rebooted after proper installation on the valve. You can reboot the device in its web panel, go to 'Settings' > 'Device Reboot'.",
|
||||
"title": "Shelly device {device_name} is not calibrated"
|
||||
},
|
||||
"open_wifi_ap": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"issue_ignored": "Issue ignored"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Your Shelly device {device_name} with IP address {ip_address} has an open WiFi access point enabled without a password. This is a security risk as anyone nearby can connect to the device.\n\nNote: If you disable the access point, the device may need to restart.",
|
||||
"menu_options": {
|
||||
"confirm": "Disable WiFi access point",
|
||||
"ignore": "Ignore"
|
||||
},
|
||||
"title": "[%key:component::shelly::issues::open_wifi_ap::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Open WiFi access point on {device_name}"
|
||||
},
|
||||
"outbound_websocket_incorrectly_enabled": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
|
||||
@@ -576,7 +576,7 @@ def _mock_rpc_device(version: str | None = None):
|
||||
zigbee_enabled=False,
|
||||
zigbee_firmware=False,
|
||||
ip_address="10.10.10.10",
|
||||
wifi_setconfig=AsyncMock(return_value={}),
|
||||
wifi_setconfig=AsyncMock(return_value={"restart_required": True}),
|
||||
ble_setconfig=AsyncMock(return_value={"restart_required": False}),
|
||||
shutdown=AsyncMock(),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from homeassistant.components.shelly.const import (
|
||||
CONF_BLE_SCANNER_MODE,
|
||||
DEPRECATED_FIRMWARE_ISSUE_ID,
|
||||
DOMAIN,
|
||||
OPEN_WIFI_AP_ISSUE_ID,
|
||||
OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID,
|
||||
BLEScannerMode,
|
||||
DeprecatedFirmwareInfo,
|
||||
@@ -254,3 +255,207 @@ async def test_deprecated_firmware_issue(
|
||||
# Assert the issue is no longer present
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test repair issues handling for open WiFi AP."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "pass": ""}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "create_entry"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False}
|
||||
assert mock_rpc_device.trigger_reboot.call_count == 1
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue_no_restart(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test repair issues handling for open WiFi AP when restart not required."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "pass": ""}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
mock_rpc_device.wifi_setconfig.return_value = {"restart_required": False}
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "create_entry"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False}
|
||||
assert mock_rpc_device.trigger_reboot.call_count == 0
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")]
|
||||
)
|
||||
async def test_open_wifi_ap_issue_exc(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test repair issues handling when wifi_setconfig ends with an exception."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "pass": ""}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
mock_rpc_device.wifi_setconfig.side_effect = exception
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"})
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "cannot_connect"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 1
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
|
||||
async def test_no_open_wifi_ap_issue_with_password(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test no repair issue is created when WiFi AP has a password."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "pass": "secure_password"}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_no_open_wifi_ap_issue_when_disabled(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test no repair issue is created when WiFi AP is disabled."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": False, "pass": ""}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_open_wifi_ap_issue_ignore(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test ignoring the open WiFi AP issue."""
|
||||
monkeypatch.setitem(
|
||||
mock_rpc_device.config,
|
||||
"wifi",
|
||||
{"ap": {"enable": True, "pass": ""}},
|
||||
)
|
||||
|
||||
issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC)
|
||||
assert await async_setup_component(hass, "repairs", {})
|
||||
await hass.async_block_till_done()
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 1
|
||||
|
||||
await async_process_repairs_platforms(hass)
|
||||
client = await hass_client()
|
||||
result = await start_repair_fix_flow(client, DOMAIN, issue_id)
|
||||
|
||||
flow_id = result["flow_id"]
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] == "menu"
|
||||
|
||||
result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "ignore"})
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "issue_ignored"
|
||||
assert mock_rpc_device.wifi_setconfig.call_count == 0
|
||||
|
||||
assert issue_registry.async_get_issue(DOMAIN, issue_id).dismissed_version
|
||||
|
||||
Reference in New Issue
Block a user