mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add Zimi Cloud Connect Integration (#129876)
* Give entry unique id with MAC, strings.json tweaks * Update codeowners * Add config_flow tests * Update requirements * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Store controller reference in entry.runtime_data instead of hass.data * Add typing * Removed hass data pop on unload. (No longer needed when hass data moved for runtime_data) * Refactor config_flow based on feedback from @zweckj with inline validation, simpler defaults, better description data * Add Michael to codeowners * Remove manual debug override in entity * Populate via_device * remove empty keys from manifest.json * Refactor with DataUpdateCoordinator Device Entities use existing push update method * set via_device to match zcc identifier * Changed logger to use debug level * Define the zimi constants * Move extraaneous code out from try * Move __del__ to async_wil_remove_from_hass * Use zcc device for name * Print debug if mac mismatch Add final exception if api is not ready after connect * Re-work configuration flow: 1. Remove unused CONF_TIMEOUT, CONF_VERBOSITY and CONF_WATCHDOG 2. Move connect() logic out of ZimiCoordinator 3. Add fast connect check during ConfigFlow to check mac matches 4. Use zcc version 3.2.3 with default watchdog time value (and remove this from HA) * Add error detail to mac mismatch * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/const.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/coordinator.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/coordinator.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove coordinator and move setup to __init__ * Set name in _attr_name * Use _light directly for status etc; Remove _state and _brightness; SImplify update() * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * No need to delete device, fix return Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove non-failing items from try Abort duplicate configurations Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Move attr change to notify Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Remove superflous defalt * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Move aysnc_connect_to_controller to helpers.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Invert if api Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Added ZimiConfigEntry to type runtime_data correctly. Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Use _abort_if_unique_id_configured Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Invert error logic for cleaner flow Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> * Add ZimiDimmer class * Set colour_mode only in ZimiDimmer * Use device name instead of entity name Update deviceinfo for zcc Update deviceinfo for lights More ZimiDimmer and ZimiLight cleanup * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/__init__.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Add missing import for CONNECTION_NETWORK_MAC * @mhannon11 Fixed some minor style changes BUT these tests need re-working now that the config_flow has a second call to the zcc helper to check the API. The tests as written now fail with connect_fail * Remove some code from try * Moved static items from initialiser * Remove superflous assert when unloading entry * refactor - move title out of data * One call to async_add_entities Update ZimiDimmer to initialise color_modes after calling super() * Create ZimiEntity base class (as ToggleEntity) * Updated test of config_flow * Move api_mock parameters to test cases * Much improved tests * Test for input value mismatch and then recovery of flow * Import FlowResultType * Implement Entities event setup correctly * Initial quality_scale.yml * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/manifest.json Co-authored-by: Josef Zweck <josef@zweck.dev> * Add link to zcc repo * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Removed unecessary f-strings * Filled in all of the quality scale * Updated in line with latest documentation improvements * FIx missing import for Entity * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/strings.json Co-authored-by: Josef Zweck <josef@zweck.dev> * Simplify logger and throw * Update homeassistant/components/zimi/helpers.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Re-factor config_flow with multi-stage steps * Add comments to notify * Don't set hw_version * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck <josef@zweck.dev> * mark docs-troubleshooting done * Update with zcc-helper version supporting PEP 625 sdist rules on PyPi * Comment re characteristic ID * Pulls in latest zcc that closes UDP listening port correctly after discovery timeout * Re-factored config_flow 1. Try discovery and auto-populate 2. Try manual configuration (with optional values for port and mac) In most cases, auto-discovery does it all. Discovery will only fail if UDP broadcast is not possible to/from zcc. * Do not show error message if discovery fails * Refactor with self.data and async_show_step_finish() * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/quality_scale.yml Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/entity.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/light.py Co-authored-by: Josef Zweck <josef@zweck.dev> * refactor import to use ConfigFlow * Change status for discovery * Add dynamic title to config flow * string * Revert title from form but add IP:port to static title * Automatically finish configuration if possible, if not show form * Use StrEnum instead of Exception class * Remove MAC from user forms * Disconnect api before form completion * Assign to self.mac instead of returning as detail * Updated test suite * Update test status * mark action exemptions todo * Remove mac related error cases from flow completely * Remove unused MAC error strings * Moved error details to logs Removed _error_tuple Removed error details * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * rename check_errors * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Update zcc-helper and support HA devices via zcc manufacter_info fields * Partial implementation - Use updated zcc-helper to discover multiple controllers * Config_flow with support for auto-discovery of one or more zcc or fallback to manual configuration. * Don't re-connect to api if validate_connection already did * Make fast=False is used for creation * Pull in improved zcc_helper version to address data completeness after machine_info implementation * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Josef Zweck <josef@zweck.dev> * Import and use ConfigFlowResult * Latest zcc to fix discovers() return value bug * Update config_flow.py * Update homeassistant/components/zimi/manifest.json Co-authored-by: Josef Zweck <josef@zweck.dev> * Use latest release version of 3.3 (no changes to rc4) * Improved sentence casing * Update strings.json * Update homeassistant/components/zimi/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Remove superflous logging Use Zimi network_name as ZCC name Cleanup device info inputs * Remove __del__ * Rename arguments * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Move PLATFORMS to init * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove debug at init * Update homeassistant/components/zimi/helpers.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove _attr_has_entity = False * More naming changes * Revised config_flow to use zcc-helper for validation using new zcc-helper version * Update homeassistant/components/zimi/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Removed commented enum * s/_entity/_device/g * Update homeassistant/components/zimi/entity.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/helpers.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Don't log error when raising exception * Updated tests for new config_flow * Refactor with new zcc that uses Exception classes to pass errors * Updated tests for config_flow to use Exceptions * Device name is based on model * Device name is None Maps better to ZCC concept where devices do not have a name but the individual entities have names. * Fix quality filename * Bump zcc-helper to 3.4 release version * Remove name override * Bump zcc-helper to 3.4.1 with new device_name attribute used to populate devinfo * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add missing transalation picked up by CI * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Bump zcc-helper to only classify light and dimmer controlPointType as lights * Bump to non dev version of zcc-helper * Ruff fixes * Add missing data description for pytest * Remove confusing comment * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/const.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/strings.json Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update homeassistant/components/zimi/light.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * f-strings * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Assert result type, step and errors between each step * test for duplicate entry * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Update tests/components/zimi/test_config_flow.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Remove duplicate test for discovery failure * Calculate brightness * Don't re-raise Exception in helper * Fix ruff and mypi errors * Add tests for missing connection exceptions * Added standard invalid_host and timeout strings * Explain limitations in discovery. * Update quality_scale.yaml * Update quality_scale.yaml * Removed duplicate strings with reference --------- Co-authored-by: markhannon <mark.hannon@gmail.com> Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> Co-authored-by: Josef Zweck <josef@zweck.dev> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1796,6 +1796,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/zeversolar/ @kvanzuijlen
|
||||
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
|
||||
/homeassistant/components/zimi/ @markhannon
|
||||
/tests/components/zimi/ @markhannon
|
||||
/homeassistant/components/zodiac/ @JulienTant
|
||||
/tests/components/zodiac/ @JulienTant
|
||||
/homeassistant/components/zone/ @home-assistant/core
|
||||
|
67
homeassistant/components/zimi/__init__.py
Normal file
67
homeassistant/components/zimi/__init__.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""The zcc integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from zcc import ControlPoint, ControlPointError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
|
||||
from .const import DOMAIN
|
||||
from .helpers import async_connect_to_controller
|
||||
|
||||
PLATFORMS = [Platform.LIGHT]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type ZimiConfigEntry = ConfigEntry[ControlPoint]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool:
|
||||
"""Connect to Zimi Controller and register device."""
|
||||
|
||||
try:
|
||||
api = await async_connect_to_controller(
|
||||
host=entry.data[CONF_HOST],
|
||||
port=entry.data[CONF_PORT],
|
||||
)
|
||||
|
||||
except ControlPointError as error:
|
||||
raise ConfigEntryNotReady(f"Zimi setup failed: {error}") from error
|
||||
|
||||
_LOGGER.debug("\n%s", api.describe())
|
||||
|
||||
entry.runtime_data = api
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, api.mac)},
|
||||
manufacturer=api.brand,
|
||||
name=f"{api.network_name}",
|
||||
model="Zimi Cloud Connect",
|
||||
sw_version=api.firmware_version,
|
||||
connections={(CONNECTION_NETWORK_MAC, api.mac)},
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
_LOGGER.debug("Zimi setup complete")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
api = entry.runtime_data
|
||||
api.disconnect()
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
172
homeassistant/components/zimi/config_flow.py
Normal file
172
homeassistant/components/zimi/config_flow.py
Normal file
@ -0,0 +1,172 @@
|
||||
"""Config flow for zcc integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from zcc import (
|
||||
ControlPoint,
|
||||
ControlPointCannotConnectError,
|
||||
ControlPointConnectionRefusedError,
|
||||
ControlPointDescription,
|
||||
ControlPointDiscoveryService,
|
||||
ControlPointError,
|
||||
ControlPointInvalidHostError,
|
||||
ControlPointTimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PORT = 5003
|
||||
STEP_MANUAL_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
)
|
||||
|
||||
SELECTED_HOST_AND_PORT = "selected_host_and_port"
|
||||
|
||||
|
||||
class ZimiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for zcc."""
|
||||
|
||||
api: ControlPoint = None
|
||||
api_descriptions: list[ControlPointDescription]
|
||||
data: dict[str, Any]
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial auto-discovery step."""
|
||||
|
||||
self.data = {}
|
||||
|
||||
try:
|
||||
self.api_descriptions = await ControlPointDiscoveryService().discovers()
|
||||
except ControlPointError:
|
||||
# ControlPointError is expected if no zcc are found on LAN
|
||||
return await self.async_step_manual()
|
||||
|
||||
if len(self.api_descriptions) == 1:
|
||||
self.data[CONF_HOST] = self.api_descriptions[0].host
|
||||
self.data[CONF_PORT] = self.api_descriptions[0].port
|
||||
await self.check_connection(self.data[CONF_HOST], self.data[CONF_PORT])
|
||||
return await self.create_entry()
|
||||
|
||||
return await self.async_step_selection()
|
||||
|
||||
async def async_step_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle selection of zcc to configure if multiple are discovered."""
|
||||
|
||||
errors: dict[str, str] | None = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.data[CONF_HOST] = user_input[SELECTED_HOST_AND_PORT].split(":")[0]
|
||||
self.data[CONF_PORT] = int(user_input[SELECTED_HOST_AND_PORT].split(":")[1])
|
||||
errors = await self.check_connection(
|
||||
self.data[CONF_HOST], self.data[CONF_PORT]
|
||||
)
|
||||
if not errors:
|
||||
return await self.create_entry()
|
||||
|
||||
available_options = [
|
||||
SelectOptionDict(
|
||||
label=f"{description.host}:{description.port}",
|
||||
value=f"{description.host}:{description.port}",
|
||||
)
|
||||
for description in self.api_descriptions
|
||||
]
|
||||
|
||||
available_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
SELECTED_HOST_AND_PORT, default=available_options[0]["value"]
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=available_options,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="selection", data_schema=available_schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_manual(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle manual configuration step if needed."""
|
||||
|
||||
errors: dict[str, str] | None = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.data = {**self.data, **user_input}
|
||||
|
||||
errors = await self.check_connection(
|
||||
self.data[CONF_HOST], self.data[CONF_PORT]
|
||||
)
|
||||
|
||||
if not errors:
|
||||
return await self.create_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_MANUAL_DATA_SCHEMA, self.data
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def check_connection(self, host: str, port: int) -> dict[str, str] | None:
|
||||
"""Check connection to zcc.
|
||||
|
||||
Stores mac and returns None if successful, otherwise returns error message.
|
||||
"""
|
||||
|
||||
try:
|
||||
result = await ControlPointDiscoveryService().validate_connection(
|
||||
self.data[CONF_HOST], self.data[CONF_PORT]
|
||||
)
|
||||
except ControlPointInvalidHostError:
|
||||
return {"base": "invalid_host"}
|
||||
except ControlPointConnectionRefusedError:
|
||||
return {"base": "connection_refused"}
|
||||
except ControlPointCannotConnectError:
|
||||
return {"base": "cannot_connect"}
|
||||
except ControlPointTimeoutError:
|
||||
return {"base": "timeout"}
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
return {"base": "unknown"}
|
||||
|
||||
self.data[CONF_MAC] = format_mac(result.mac)
|
||||
|
||||
return None
|
||||
|
||||
async def create_entry(self) -> ConfigFlowResult:
|
||||
"""Create entry for zcc."""
|
||||
|
||||
await self.async_set_unique_id(self.data[CONF_MAC])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=f"ZIMI Controller ({self.data[CONF_HOST]}:{self.data[CONF_PORT]})",
|
||||
data=self.data,
|
||||
)
|
3
homeassistant/components/zimi/const.py
Normal file
3
homeassistant/components/zimi/const.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""Constants for the zcc integration."""
|
||||
|
||||
DOMAIN = "zimi"
|
66
homeassistant/components/zimi/entity.py
Normal file
66
homeassistant/components/zimi/entity.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Base entity for zimi integrations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from zcc import ControlPoint
|
||||
from zcc.device import ControlPointDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZimiEntity(Entity):
|
||||
"""Representation of a Zimi API entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None:
|
||||
"""Initialize an HA Entity which is a ZimiDevice."""
|
||||
|
||||
self._device = device
|
||||
self._attr_unique_id = self._device.identifier
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device.manufacture_info.identifier)},
|
||||
manufacturer=self._device.manufacture_info.manufacturer,
|
||||
model=self._device.manufacture_info.model,
|
||||
name=self._device.manufacture_info.name,
|
||||
hw_version=device.manufacture_info.hwVersion,
|
||||
sw_version=device.manufacture_info.firmwareVersion,
|
||||
suggested_area=device.room,
|
||||
via_device=(DOMAIN, api.mac),
|
||||
)
|
||||
self._attr_name = self._device.name.strip()
|
||||
self._attr_suggested_area = self._device.room
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if Home Assistant is able to read the state and control the underlying device."""
|
||||
return self._device.is_connected
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to the events."""
|
||||
await super().async_added_to_hass()
|
||||
self._device.subscribe(self)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Cleanup ZimiLight with removal of notification prior to removal."""
|
||||
self._device.unsubscribe(self)
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
def notify(self, _observable: object) -> None:
|
||||
"""Receive notification from device that state has changed.
|
||||
|
||||
No data is fetched for the notification but schedule_update_ha_state is called.
|
||||
"""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Received notification() for %s in %s", self._device.name, self._device.room
|
||||
)
|
||||
self.schedule_update_ha_state(force_refresh=True)
|
38
homeassistant/components/zimi/helpers.py
Normal file
38
homeassistant/components/zimi/helpers.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""The zcc integration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from zcc import ControlPoint, ControlPointDescription
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_connect_to_controller(
|
||||
host: str, port: int, fast: bool = False
|
||||
) -> ControlPoint:
|
||||
"""Connect to Zimi Cloud Controller with defined parameters."""
|
||||
|
||||
_LOGGER.debug("Connecting to %s:%d", host, port)
|
||||
|
||||
api = ControlPoint(
|
||||
description=ControlPointDescription(
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
)
|
||||
await api.connect(fast=fast)
|
||||
|
||||
if api.ready:
|
||||
_LOGGER.debug("Connected")
|
||||
|
||||
if not fast:
|
||||
api.start_watchdog()
|
||||
_LOGGER.debug("Started watchdog")
|
||||
|
||||
return api
|
||||
|
||||
raise ConfigEntryNotReady("Connection failed: not ready")
|
103
homeassistant/components/zimi/light.py
Normal file
103
homeassistant/components/zimi/light.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Light platform for zcc integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from zcc import ControlPoint
|
||||
from zcc.device import ControlPointDevice
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import ZimiConfigEntry
|
||||
from .entity import ZimiEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ZimiConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Zimi Light platform."""
|
||||
|
||||
api = config_entry.runtime_data
|
||||
|
||||
lights: list[ZimiLight | ZimiDimmer] = [
|
||||
ZimiLight(device, api) for device in api.lights if device.type != "dimmer"
|
||||
]
|
||||
|
||||
lights.extend(
|
||||
[ZimiDimmer(device, api) for device in api.lights if device.type == "dimmer"]
|
||||
)
|
||||
|
||||
async_add_entities(lights)
|
||||
|
||||
|
||||
class ZimiLight(ZimiEntity, LightEntity):
|
||||
"""Representation of a Zimi Light."""
|
||||
|
||||
def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None:
|
||||
"""Initialize a ZimiLight."""
|
||||
|
||||
super().__init__(device, api)
|
||||
|
||||
self._attr_color_mode = ColorMode.ONOFF
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return self._device.is_on
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on (with optional brightness)."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Sending turn_on() for %s in %s", self._device.name, self._device.room
|
||||
)
|
||||
|
||||
await self._device.turn_on()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn off."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Sending turn_off() for %s in %s", self._device.name, self._device.room
|
||||
)
|
||||
|
||||
await self._device.turn_off()
|
||||
|
||||
|
||||
class ZimiDimmer(ZimiLight):
|
||||
"""Zimi Light supporting dimming."""
|
||||
|
||||
def __init__(self, device: ControlPointDevice, api: ControlPoint) -> None:
|
||||
"""Initialize a ZimiDimmer."""
|
||||
super().__init__(device, api)
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
if self._device.type != "dimmer":
|
||||
raise ValueError("ZimiDimmer needs a dimmable light")
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Instruct the light to turn on (with optional brightness)."""
|
||||
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, 255) * 100 / 255
|
||||
_LOGGER.debug(
|
||||
"Sending turn_on(brightness=%d) for %s in %s",
|
||||
brightness,
|
||||
self._device.name,
|
||||
self._device.room,
|
||||
)
|
||||
|
||||
await self._device.set_brightness(brightness)
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of the light."""
|
||||
return round(self._device.brightness * 255 / 100)
|
10
homeassistant/components/zimi/manifest.json
Normal file
10
homeassistant/components/zimi/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "zimi",
|
||||
"name": "zimi",
|
||||
"codeowners": ["@markhannon"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zimi",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["zcc-helper==3.5"]
|
||||
}
|
99
homeassistant/components/zimi/quality_scale.yaml
Normal file
99
homeassistant/components/zimi/quality_scale.yaml
Normal file
@ -0,0 +1,99 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
There are no service actions.
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: |
|
||||
There is no polling of the entities.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency:
|
||||
status: done
|
||||
comment: |
|
||||
https://mark_hannon@bitbucket.org/mark_hannon/zcc.git
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
There are no service actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
config-entry-unloading: done
|
||||
log-when-unavailable: todo
|
||||
entity-unavailable: done
|
||||
action-exceptions: todo
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
There is no user authentication needed.
|
||||
parallel-updates:
|
||||
status: todo
|
||||
comment: |
|
||||
Test of parallel updates will be done before setting.
|
||||
test-coverage: todo
|
||||
integration-owner: done
|
||||
docs-installation-parameters: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration has no options flow
|
||||
|
||||
# Gold
|
||||
entity-translations: todo
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: |
|
||||
Will set device classes for subsequent entities - not relevant for light.
|
||||
devices: done
|
||||
entity-category: todo
|
||||
entity-disabled-by-default: todo
|
||||
discovery:
|
||||
status: todo
|
||||
comment: >
|
||||
Discovery is supported for the case where the Zimi Cloud Controller(s) are
|
||||
connected to a local LAN network. Discover is not supported if the Zimi
|
||||
Cloud Controller(s) are not connected to the local LAN network.
|
||||
stale-devices: todo
|
||||
diagnostics: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices:
|
||||
status: todo
|
||||
comment: |
|
||||
New devices will be automatically added - but only when the zcc connection is re-established.
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: >
|
||||
Discovery is not supported.
|
||||
repair-issues: todo
|
||||
docs-use-cases: todo
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-data-update: done
|
||||
docs-known-limitations: done
|
||||
docs-examples: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not use web sessions.
|
||||
strict-typing:
|
||||
status: todo
|
46
homeassistant/components/zimi/strings.json
Normal file
46
homeassistant/components/zimi/strings.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Zimi - Discover device(s)",
|
||||
"description": "Discover and auto-configure Zimi Cloud Connect device."
|
||||
},
|
||||
"selection": {
|
||||
"title": "Zimi - Select device",
|
||||
"description": "Select Zimi Cloud Connect device to configure.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"selected_host_and_port": "Selected ZCC"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Mandatory - ZCC IP address.",
|
||||
"port": "Mandatory - ZCC port number (default=5003).",
|
||||
"selected_host_and_port": "Selected ZCC IP address and port number"
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"title": "Zimi - Configure device",
|
||||
"description": "Enter details of your Zimi Cloud Connect device.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::zimi::config::step::selection::data_description::host%]",
|
||||
"port": "[%key:component::zimi::config::step::selection::data_description::port%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"timeout": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"connection_refused": "Connection refused"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
}
|
||||
}
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@ -736,6 +736,7 @@ FLOWS = {
|
||||
"zerproc",
|
||||
"zeversolar",
|
||||
"zha",
|
||||
"zimi",
|
||||
"zodiac",
|
||||
"zwave_js",
|
||||
"zwave_me",
|
||||
|
@ -7630,6 +7630,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"zimi": {
|
||||
"name": "zimi",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"zodiac": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
|
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@ -3155,6 +3155,9 @@ zabbix-utils==2.0.2
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zimi
|
||||
zcc-helper==3.5
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.147.0
|
||||
|
||||
|
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@ -2554,6 +2554,9 @@ yt-dlp[default]==2025.03.31
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zimi
|
||||
zcc-helper==3.5
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.147.0
|
||||
|
||||
|
1
tests/components/zimi/__init__.py
Normal file
1
tests/components/zimi/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the zimi component."""
|
371
tests/components/zimi/test_config_flow.py
Normal file
371
tests/components/zimi/test_config_flow.py
Normal file
@ -0,0 +1,371 @@
|
||||
"""Tests for the zimi config flow."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from zcc import (
|
||||
ControlPointCannotConnectError,
|
||||
ControlPointConnectionRefusedError,
|
||||
ControlPointDescription,
|
||||
ControlPointError,
|
||||
ControlPointInvalidHostError,
|
||||
ControlPointTimeoutError,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.zimi.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
INPUT_MAC = "aa:bb:cc:dd:ee:ff"
|
||||
INPUT_MAC_EXTRA = "aa:bb:cc:dd:ee:ee"
|
||||
INPUT_HOST = "192.168.1.100"
|
||||
INPUT_HOST_EXTRA = "192.168.1.101"
|
||||
INPUT_PORT = 5003
|
||||
INPUT_PORT_EXTRA = 5004
|
||||
|
||||
INVALID_INPUT_MAC = "xyz"
|
||||
MISMATCHED_INPUT_MAC = "aa:bb:cc:dd:ee:ee"
|
||||
SELECTED_HOST_AND_PORT = "selected_host_and_port"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def discovery_mock():
|
||||
"""Mock the ControlPointDiscoveryService."""
|
||||
with patch(
|
||||
"homeassistant.components.zimi.config_flow.ControlPointDiscoveryService",
|
||||
autospec=True,
|
||||
) as mock:
|
||||
mock.return_value = mock
|
||||
yield mock
|
||||
|
||||
|
||||
async def test_user_discovery_success(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test user form transitions to creation if zcc discovery succeeds."""
|
||||
|
||||
discovery_mock.discovers.return_value = [
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT)
|
||||
]
|
||||
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST,
|
||||
"port": INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
}
|
||||
|
||||
|
||||
async def test_user_discovery_success_selection(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test user form transitions via selection to creation if zcc discovery succeeds has multiple hosts."""
|
||||
|
||||
discovery_mock.discovers.return_value = [
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT),
|
||||
ControlPointDescription(host=INPUT_HOST_EXTRA, port=INPUT_PORT_EXTRA),
|
||||
]
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "selection"
|
||||
assert result["errors"] == {}
|
||||
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(
|
||||
host=INPUT_HOST_EXTRA, port=INPUT_PORT_EXTRA, mac=INPUT_MAC_EXTRA
|
||||
)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
SELECTED_HOST_AND_PORT: f"{INPUT_HOST_EXTRA}:{INPUT_PORT_EXTRA!s}",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST_EXTRA,
|
||||
"port": INPUT_PORT_EXTRA,
|
||||
"mac": format_mac(INPUT_MAC_EXTRA),
|
||||
}
|
||||
|
||||
|
||||
async def test_user_discovery_duplicates(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test that flow is aborted if duplicates are added."""
|
||||
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=INPUT_MAC,
|
||||
data={
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
},
|
||||
).add_to_hass(hass)
|
||||
|
||||
discovery_mock.discovers.return_value = [
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT)
|
||||
]
|
||||
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_finish_manual_success(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test manual form transitions to creation with valid data."""
|
||||
|
||||
discovery_mock.discovers.side_effect = ControlPointError("Discovery failed")
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})"
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST,
|
||||
"port": INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
}
|
||||
|
||||
|
||||
async def test_manual_cannot_connect(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test manual form transitions via cannot_connect to creation."""
|
||||
|
||||
discovery_mock.discovers.side_effect = ControlPointError("Discovery failed")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {}
|
||||
|
||||
# First attempt fails with CANNOT_CONNECT when attempting to connect
|
||||
discovery_mock.return_value.validate_connection.side_effect = (
|
||||
ControlPointCannotConnectError
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Second attempt succeeds
|
||||
discovery_mock.return_value.validate_connection.side_effect = None
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})"
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST,
|
||||
"port": INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
}
|
||||
|
||||
|
||||
async def test_manual_gethostbyname_error(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
) -> None:
|
||||
"""Test manual form transitions via gethostbyname failure to creation."""
|
||||
|
||||
discovery_mock.discovers.side_effect = ControlPointError("Discovery failed")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {}
|
||||
|
||||
# First attempt fails with name lookup failure when attempting to connect
|
||||
discovery_mock.return_value.validate_connection.side_effect = (
|
||||
ControlPointInvalidHostError
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"]
|
||||
assert result["errors"] == {"base": "invalid_host"}
|
||||
|
||||
# Second attempt succeeds
|
||||
discovery_mock.return_value.validate_connection.side_effect = None
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})"
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST,
|
||||
"port": INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_expected"),
|
||||
[
|
||||
(
|
||||
ControlPointInvalidHostError,
|
||||
{"base": "invalid_host"},
|
||||
),
|
||||
(
|
||||
ControlPointConnectionRefusedError,
|
||||
{"base": "connection_refused"},
|
||||
),
|
||||
(
|
||||
ControlPointCannotConnectError,
|
||||
{"base": "cannot_connect"},
|
||||
),
|
||||
(
|
||||
ControlPointTimeoutError,
|
||||
{"base": "timeout"},
|
||||
),
|
||||
(
|
||||
Exception,
|
||||
{"base": "unknown"},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_manual_connection_errors(
|
||||
hass: HomeAssistant,
|
||||
discovery_mock: MagicMock,
|
||||
side_effect: Exception,
|
||||
error_expected: dict,
|
||||
) -> None:
|
||||
"""Test manual form connection errors."""
|
||||
|
||||
discovery_mock.discovers.side_effect = ControlPointError("Discovery failed")
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == {}
|
||||
|
||||
# First attempt fails with connection errors
|
||||
discovery_mock.return_value.validate_connection.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "manual"
|
||||
assert result["errors"] == error_expected
|
||||
|
||||
# Second attempt succeeds
|
||||
discovery_mock.return_value.validate_connection.side_effect = None
|
||||
discovery_mock.return_value.validate_connection.return_value = (
|
||||
ControlPointDescription(host=INPUT_HOST, port=INPUT_PORT, mac=INPUT_MAC)
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: INPUT_HOST,
|
||||
CONF_PORT: INPUT_PORT,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"ZIMI Controller ({INPUT_HOST}:{INPUT_PORT})"
|
||||
assert result["data"] == {
|
||||
"host": INPUT_HOST,
|
||||
"port": INPUT_PORT,
|
||||
"mac": format_mac(INPUT_MAC),
|
||||
}
|
Reference in New Issue
Block a user