Skip to content

Commit f840a62

Browse files
committed
Merge of latest core integration PR
1 parent d342dde commit f840a62

File tree

13 files changed

+477
-633
lines changed

13 files changed

+477
-633
lines changed

custom_components/zimi/__init__.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,75 @@
11
"""The zcc integration."""
2+
23
from __future__ import annotations
34

45
import logging
56

7+
from zcc import ControlPoint, ControlPointError
8+
69
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.const import CONF_HOST, CONF_PORT
711
from homeassistant.core import HomeAssistant
12+
from homeassistant.exceptions import ConfigEntryNotReady
813
from homeassistant.helpers import device_registry as dr
14+
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
915

10-
from .const import CONTROLLER, DOMAIN, VERBOSITY
11-
from .controller import ZimiController
16+
from .const import DOMAIN, PLATFORMS
17+
from .helpers import async_connect_to_controller
1218

1319
_LOGGER = logging.getLogger(__name__)
1420

1521

16-
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
17-
"""Connect to Zimi Controller and register device."""
22+
type ZimiConfigEntry = ConfigEntry[ControlPoint]
1823

19-
if entry.data.get(VERBOSITY, 0) > 1:
20-
_LOGGER.setLevel(logging.DEBUG)
2124

25+
async def async_setup_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool:
26+
"""Connect to Zimi Controller and register device."""
2227
_LOGGER.debug("Zimi setup starting")
2328

24-
controller = ZimiController(hass, entry)
25-
connected = await controller.connect()
26-
if not connected:
27-
return False
29+
try:
30+
api = await async_connect_to_controller(
31+
host=entry.data[CONF_HOST],
32+
port=entry.data[CONF_PORT],
33+
)
34+
35+
except ControlPointError as error:
36+
_LOGGER.error("Initiation failed: %s", error)
37+
raise ConfigEntryNotReady(error) from error
38+
39+
if not api:
40+
msg = "Zimi setup failed: not ready"
41+
_LOGGER.error(msg=msg)
42+
raise ConfigEntryNotReady(msg)
2843

29-
hass.data[CONTROLLER] = controller
44+
_LOGGER.debug("\n%s", api.describe())
45+
46+
entry.runtime_data = api
3047

3148
device_registry = dr.async_get(hass)
3249
device_registry.async_get_or_create(
3350
config_entry_id=entry.entry_id,
34-
identifiers={(DOMAIN, controller.controller.mac)},
35-
manufacturer=controller.controller.brand,
36-
name=f"Zimi({controller.controller.host}:{controller.controller.port})",
37-
model=controller.controller.product,
38-
sw_version="unknown",
51+
identifiers={(DOMAIN, api.mac)},
52+
manufacturer=api.brand,
53+
name=f"ZCC ({api.host}:{api.port})",
54+
model=api.product,
55+
model_id="Zimi Cloud Connect",
56+
hw_version=api.firmware_version,
57+
sw_version=api.api_version,
58+
connections={(CONNECTION_NETWORK_MAC, api.mac)},
3959
)
4060

61+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
62+
4163
_LOGGER.debug("Zimi setup complete")
4264

4365
return True
66+
67+
68+
async def async_unload_entry(hass: HomeAssistant, entry: ZimiConfigEntry) -> bool:
69+
"""Unload a config entry."""
70+
71+
api = entry.runtime_data
72+
if api:
73+
api.disconnect()
74+
75+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
Lines changed: 100 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,130 @@
11
"""Config flow for zcc integration."""
2+
23
from __future__ import annotations
34

45
import logging
56
import socket
67
from typing import Any
78

8-
import voluptuous as vol # type: ignore[import]
9-
from zcc import ControlPointDiscoveryService, ControlPointError # type: ignore[import]
9+
import voluptuous as vol
10+
from zcc import ControlPointDiscoveryService, ControlPointError
1011

1112
from homeassistant import config_entries
12-
from homeassistant.const import CONF_HOST, CONF_PORT
13-
from homeassistant.data_entry_flow import FlowResult
14-
from homeassistant.exceptions import HomeAssistantError
13+
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT
14+
from homeassistant.exceptions import ConfigEntryNotReady
15+
from homeassistant.helpers.device_registry import format_mac
1516

16-
from .const import DOMAIN, TIMEOUT, VERBOSITY, WATCHDOG
17+
from . import async_connect_to_controller
18+
from .const import DOMAIN, TITLE
1719

1820
_LOGGER = logging.getLogger(__name__)
1921

2022
STEP_USER_DATA_SCHEMA = vol.Schema(
2123
{
22-
vol.Optional(CONF_HOST, default=""): str,
24+
vol.Optional(CONF_HOST): str,
2325
vol.Optional(CONF_PORT, default=5003): int,
24-
vol.Optional(TIMEOUT, default=3): int,
25-
vol.Optional(WATCHDOG, default=1800): int,
26-
vol.Optional(VERBOSITY, default=1): int,
26+
vol.Required(CONF_MAC): str,
2727
}
2828
)
2929

3030

31-
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
32-
"""Handle a config flow for zcc."""
31+
class ZimiConfigException(Exception):
32+
"""Base class for config exceptions."""
3333

34-
VERSION = 1
34+
35+
class ZimiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
36+
"""Handle a config flow for zcc."""
3537

3638
async def async_step_user(
3739
self, user_input: dict[str, Any] | None = None
38-
) -> FlowResult:
40+
) -> config_entries.ConfigFlowResult:
3941
"""Handle the initial step."""
40-
if user_input is None:
41-
return self.async_show_form(
42-
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
43-
)
44-
45-
data = {}
46-
errors = {}
47-
48-
try:
49-
data = await self.validate_input(user_input)
50-
except CannotConnect:
51-
errors["base"] = "cannot_connect"
52-
except ConnectionRefused:
53-
errors["base"] = "connection_refused"
54-
except DiscoveryFailure:
55-
errors["base"] = "discovery_failure"
56-
except InvalidAuth:
57-
errors["base"] = "invalid_auth"
58-
except InvalidHost:
59-
errors["base"] = "invalid_host"
60-
except TimeOut:
61-
errors["base"] = "timeout"
62-
except Exception: # pylint: disable=broad-except
63-
_LOGGER.exception("Unexpected exception during configuration steps")
64-
errors["base"] = "unknown"
65-
else:
66-
return self.async_create_entry(title=data["title"], data=data)
67-
68-
return self.async_show_form(
69-
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
70-
)
7142

72-
async def validate_input(self, data: dict[str, Any]) -> dict[str, Any]:
73-
"""Validate the user input."""
43+
api = None
44+
errors: dict[str, str] = {}
45+
description_placeholders: dict[str, str] = {}
7446

75-
if data[TIMEOUT] is None:
76-
data[TIMEOUT] = 3
47+
if user_input is not None:
48+
data: dict[str, Any] = {}
7749

78-
if data[VERBOSITY] is None:
79-
data[VERBOSITY] = 1
80-
81-
if data[WATCHDOG] is None:
82-
data[WATCHDOG] = 1800
83-
84-
if data[CONF_HOST] == "":
85-
try:
86-
description = await ControlPointDiscoveryService().discover()
87-
data[CONF_HOST] = description.host
88-
data[CONF_PORT] = description.port
89-
except ControlPointError as e:
90-
raise DiscoveryFailure(e) from e
91-
else:
9250
try:
93-
socket.gethostbyname(data[CONF_HOST])
94-
except socket.herror as e:
95-
raise InvalidHost("%s is not a valid host" % data[CONF_HOST]) from e
96-
try:
97-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
98-
s.settimeout(10)
99-
s.connect((data[CONF_HOST], int(data[CONF_PORT])))
100-
except ConnectionRefusedError as e:
101-
raise ConnectionRefused() from e
102-
except TimeoutError as e:
103-
raise TimeOut() from e
104-
except Exception as e:
105-
raise CannotConnect() from e
106-
107-
# Return info that you want to store in the config entry.
108-
return {
109-
"title": "ZIMI Controller",
110-
"host": data[CONF_HOST],
111-
"port": data[CONF_PORT],
112-
"timeout": data[TIMEOUT],
113-
"verbosity": data[VERBOSITY],
114-
"watchdog": data[WATCHDOG],
115-
}
116-
51+
if not user_input[CONF_HOST]:
52+
try:
53+
description = await ControlPointDiscoveryService().discover()
54+
except ControlPointError as e:
55+
errors["base"] = "discovery_failure"
56+
raise ZimiConfigException(errors["base"]) from e
57+
data[CONF_HOST] = description.host
58+
data[CONF_PORT] = description.port
59+
else:
60+
hostbyname = None
61+
data[CONF_HOST] = user_input[CONF_HOST]
62+
data[CONF_PORT] = user_input[CONF_PORT]
63+
try:
64+
hostbyname = socket.gethostbyname(data[CONF_HOST])
65+
except socket.gaierror as e:
66+
errors["base"] = "invalid_host"
67+
raise ZimiConfigException(errors["base"]) from e
68+
if hostbyname:
69+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
70+
s.settimeout(10)
71+
try:
72+
s.connect((data[CONF_HOST], data[CONF_PORT]))
73+
s.close()
74+
except ConnectionRefusedError as e:
75+
errors["base"] = "connection_refused"
76+
raise ZimiConfigException(errors["base"]) from e
77+
except TimeoutError as e:
78+
errors["base"] = "timeout"
79+
raise ZimiConfigException(errors["base"]) from e
80+
except socket.gaierror as e:
81+
errors["base"] = "cannot_connect"
82+
raise ZimiConfigException(errors["base"]) from e
83+
else:
84+
errors["base"] = "invalid_host"
85+
raise ZimiConfigException(errors["base"]) # noqa: TRY301
86+
87+
data[CONF_MAC] = format_mac(user_input[CONF_MAC])
88+
if data[CONF_MAC] is user_input[CONF_MAC]:
89+
errors["base"] = "invalid_mac"
90+
raise ZimiConfigException(errors["base"]) # noqa: TRY301
91+
92+
try:
93+
api = await async_connect_to_controller(
94+
data[CONF_HOST], data[CONF_PORT], fast=True
95+
)
96+
except ConfigEntryNotReady as e:
97+
errors["base"] = "cannot_connect"
98+
raise ZimiConfigException(errors["base"]) from e
99+
100+
if api:
101+
if data[CONF_MAC] != format_mac(api.mac):
102+
msg = f"{data[CONF_MAC]} != {format_mac(api.mac)}"
103+
_LOGGER.error("Configured mac mismatch: %s", msg)
104+
errors["base"] = "mismatched_mac"
105+
description_placeholders["error_detail"] = msg
106+
raise ZimiConfigException(errors["base"]) # noqa: TRY301
107+
else:
108+
errors["base"] = "cannot_connect"
109+
raise ZimiConfigException(errors["base"]) # noqa: TRY301
110+
111+
except ZimiConfigException:
112+
_LOGGER.exception("Exception during configuration steps")
113+
except Exception: # pylint: disable=broad-except
114+
_LOGGER.exception("Unexpected exception during configuration steps")
115+
errors["base"] = "unknown"
116+
117+
if api:
118+
api.disconnect()
119+
120+
if not errors:
121+
await self.async_set_unique_id(data[CONF_MAC])
122+
self._abort_if_unique_id_configured()
123+
return self.async_create_entry(title=TITLE, data=data)
117124

118-
class CannotConnect(HomeAssistantError):
119-
"""Error to indicate we cannot connect."""
120-
121-
122-
class ConnectionRefused(HomeAssistantError):
123-
"""Error to indicate connection was refused."""
124-
125-
126-
class DiscoveryFailure(HomeAssistantError):
127-
"""Error to indicate that Zimi UDP discovery failed."""
128-
129-
130-
class InvalidAuth(HomeAssistantError):
131-
"""Error to indicate there is invalid auth."""
132-
133-
134-
class InvalidHost(HomeAssistantError):
135-
"""Error to indicate that host IP address is invalid."""
136-
137-
138-
class TimeOut(HomeAssistantError):
139-
"""Error to indicate timeout when attempting to connect."""
125+
return self.async_show_form(
126+
step_id="user",
127+
data_schema=STEP_USER_DATA_SCHEMA,
128+
errors=errors,
129+
description_placeholders=description_placeholders,
130+
)

custom_components/zimi/const.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Constants for the zcc integration."""
22

3+
from homeassistant.const import Platform
4+
35
CONTROLLER = "zimi_controller"
46
DOMAIN = "zimi"
5-
PLATFORMS = ["light", "sensor", "switch", "cover", "fan"]
6-
7-
TIMEOUT = "timeout"
8-
VERBOSITY = "verbosity"
9-
WATCHDOG = "watchdog"
7+
PLATFORMS = [
8+
Platform.COVER,
9+
Platform.FAN,
10+
Platform.LIGHT,
11+
Platform.SENSOR,
12+
Platform.SWITCH,
13+
]
14+
TITLE = "ZIMI Controller"

0 commit comments

Comments
 (0)