|
4 | 4 |
|
5 | 5 | import voluptuous as vol |
6 | 6 | from homeassistant import config_entries |
| 7 | +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult |
| 8 | +from homeassistant.components import zeroconf |
7 | 9 | from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS |
8 | 10 | from homeassistant.const import (CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD, |
9 | 11 | CONF_PORT, CONF_USERNAME) |
10 | 12 | from homeassistant.helpers.device_registry import format_mac |
| 13 | +from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo |
| 14 | + |
| 15 | +from typing import Any |
11 | 16 |
|
12 | 17 | from .common import get_status |
13 | 18 | from .const import (ALLWINNER, ALLWINNER_R, ALLWINNERV2, ALLWINNERV2_R, |
|
35 | 40 | ): vol.In(["auto", "disabled", "x 2", "x 3", "x 4", "x 5"]) |
36 | 41 | } |
37 | 42 |
|
| 43 | +DATA_SCHEMA_ZC = { |
| 44 | + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): str, |
| 45 | + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): str, |
| 46 | + vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_EXTRA_ARGUMENTS): str, |
| 47 | + vol.Required( |
| 48 | + CONF_BOOST_SPEAKER, |
| 49 | + default="auto", |
| 50 | + ): vol.In(["auto", "disabled", "x 2", "x 3", "x 4", "x 5"]) |
| 51 | +} |
| 52 | + |
38 | 53 | class YiHackFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): |
39 | 54 | """Handle a yi-hack config flow.""" |
40 | 55 |
|
41 | 56 | VERSION = 1 |
42 | 57 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH |
43 | 58 |
|
44 | | - async def async_step_user(self, user_input=None): |
| 59 | + def __init__(self) -> None: |
| 60 | + """Set up the instance.""" |
| 61 | + self.connection_data: dict[str, Any] = {} |
| 62 | + |
| 63 | + async def async_process_input( |
| 64 | + self, user_input: dict[str, Any] | None = None |
| 65 | + ) -> ConfigFlowResult: |
| 66 | + """Handle and complete a flow.""" |
| 67 | + errors = {} |
| 68 | + |
| 69 | + host = user_input[CONF_HOST] |
| 70 | + port = user_input[CONF_PORT] |
| 71 | + user = user_input[CONF_USERNAME] |
| 72 | + password = user_input[CONF_PASSWORD] |
| 73 | + extra_arguments = user_input[CONF_EXTRA_ARGUMENTS] |
| 74 | + boost_speaker = user_input[CONF_BOOST_SPEAKER] |
| 75 | + |
| 76 | + response = await self.hass.async_add_executor_job(get_status, user_input) |
| 77 | + if response is not None: |
| 78 | + try: |
| 79 | + serial_number = response["serial_number"] |
| 80 | + except KeyError: |
| 81 | + serial_number = None |
| 82 | + |
| 83 | + try: |
| 84 | + mac = response["mac_addr"] |
| 85 | + except KeyError: |
| 86 | + mac = None |
| 87 | + |
| 88 | + try: |
| 89 | + ptz = response["ptz"] |
| 90 | + except KeyError: |
| 91 | + ptz = "no" |
| 92 | + |
| 93 | + try: |
| 94 | + hackname = response["name"] |
| 95 | + except KeyError: |
| 96 | + hackname = DEFAULT_BRAND |
| 97 | + |
| 98 | + try: |
| 99 | + privacy = response["privacy"] |
| 100 | + _LOGGER.error("Unsupported hack version, please update your cam") |
| 101 | + return self.async_abort(reason="wrong_hack_version") |
| 102 | + except KeyError: |
| 103 | + privacy = None |
| 104 | + |
| 105 | + if serial_number is not None and mac is not None: |
| 106 | + user_input[CONF_SERIAL] = serial_number |
| 107 | + user_input[CONF_MAC] = format_mac(mac) |
| 108 | + user_input[CONF_PTZ] = ptz |
| 109 | + user_input[CONF_HACK_NAME] = hackname |
| 110 | + if hackname == MSTAR: |
| 111 | + user_input[CONF_NAME] = MSTAR_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 112 | + elif hackname == ALLWINNER: |
| 113 | + user_input[CONF_NAME] = ALLWINNER_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 114 | + elif hackname == ALLWINNERV2: |
| 115 | + user_input[CONF_NAME] = ALLWINNERV2_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 116 | + elif hackname == V5: |
| 117 | + user_input[CONF_NAME] = V5_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 118 | + elif hackname == SONOFF: |
| 119 | + user_input[CONF_NAME] = SONOFF_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 120 | + else: |
| 121 | + user_input[CONF_NAME] = DEFAULT_BRAND_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
| 122 | + else: |
| 123 | + _LOGGER.error("Unable to get mac address or serial number from device %s", host) |
| 124 | + errors["base"] = "cannot_get_mac_or_serial" |
| 125 | + |
| 126 | + if not errors: |
| 127 | + await self.async_set_unique_id(user_input[CONF_MAC]) |
| 128 | + self._abort_if_unique_id_configured() |
| 129 | + |
| 130 | + for entry in self._async_current_entries(): |
| 131 | + if entry.data[CONF_MAC] == user_input[CONF_MAC]: |
| 132 | + _LOGGER.error("Device already configured: %s", host) |
| 133 | + return self.async_abort(reason="already_configured") |
| 134 | + |
| 135 | + user_input[CONF_RTSP_PORT] = None |
| 136 | + user_input[CONF_MQTT_PREFIX] = None |
| 137 | + user_input[CONF_TOPIC_STATUS] = None |
| 138 | + user_input[CONF_TOPIC_MOTION_DETECTION] = None |
| 139 | + user_input[CONF_TOPIC_SOUND_DETECTION] = None |
| 140 | + user_input[CONF_TOPIC_MOTION_DETECTION_IMAGE] = None |
| 141 | + |
| 142 | + return self.async_create_entry( |
| 143 | + title=user_input[CONF_NAME], |
| 144 | + data=user_input |
| 145 | + ) |
| 146 | + |
| 147 | + async def async_step_user( |
| 148 | + self, user_input: dict[str, Any] | None = None |
| 149 | + ) -> ConfigFlowResult: |
45 | 150 | """Handle a flow initiated by the user.""" |
46 | 151 | errors = {} |
47 | 152 |
|
48 | 153 | if user_input is not None: |
49 | | - host = user_input[CONF_HOST] |
50 | | - port = user_input[CONF_PORT] |
51 | | - user = user_input[CONF_USERNAME] |
52 | | - password = user_input[CONF_PASSWORD] |
53 | | - extra_arguments = user_input[CONF_EXTRA_ARGUMENTS] |
54 | | - boost_speaker = user_input[CONF_BOOST_SPEAKER] |
55 | | - |
56 | | - response = await self.hass.async_add_executor_job(get_status, user_input) |
57 | | - if response is not None: |
58 | | - try: |
59 | | - serial_number = response["serial_number"] |
60 | | - except KeyError: |
61 | | - serial_number = None |
62 | | - |
63 | | - try: |
64 | | - mac = response["mac_addr"] |
65 | | - except KeyError: |
66 | | - mac = None |
67 | | - |
68 | | - try: |
69 | | - ptz = response["ptz"] |
70 | | - except KeyError: |
71 | | - ptz = "no" |
72 | | - |
73 | | - try: |
74 | | - hackname = response["name"] |
75 | | - except KeyError: |
76 | | - hackname = DEFAULT_BRAND |
77 | | - |
78 | | - try: |
79 | | - privacy = response["privacy"] |
80 | | - _LOGGER.error("Unsupported hack version, please update your cam") |
81 | | - return self.async_abort(reason="wrong_hack_version") |
82 | | - except KeyError: |
83 | | - privacy = None |
84 | | - |
85 | | - if serial_number is not None and mac is not None: |
86 | | - user_input[CONF_SERIAL] = serial_number |
87 | | - user_input[CONF_MAC] = format_mac(mac) |
88 | | - user_input[CONF_PTZ] = ptz |
89 | | - user_input[CONF_HACK_NAME] = hackname |
90 | | - if hackname == MSTAR: |
91 | | - user_input[CONF_NAME] = MSTAR_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
92 | | - elif hackname == ALLWINNER: |
93 | | - user_input[CONF_NAME] = ALLWINNER_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
94 | | - elif hackname == ALLWINNERV2: |
95 | | - user_input[CONF_NAME] = ALLWINNERV2_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
96 | | - elif hackname == V5: |
97 | | - user_input[CONF_NAME] = V5_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
98 | | - elif hackname == SONOFF: |
99 | | - user_input[CONF_NAME] = SONOFF_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
100 | | - else: |
101 | | - user_input[CONF_NAME] = DEFAULT_BRAND_R + "_" + user_input[CONF_MAC].replace(':', '')[6:] |
102 | | - else: |
103 | | - _LOGGER.error("Unable to get mac address or serial number from device %s", host) |
104 | | - errors["base"] = "cannot_get_mac_or_serial" |
105 | | - |
106 | | - if not errors: |
107 | | - await self.async_set_unique_id(user_input[CONF_MAC]) |
108 | | - self._abort_if_unique_id_configured() |
109 | | - |
110 | | - for entry in self._async_current_entries(): |
111 | | - if entry.data[CONF_MAC] == user_input[CONF_MAC]: |
112 | | - _LOGGER.error("Device already configured: %s", host) |
113 | | - return self.async_abort(reason="already_configured") |
114 | | - |
115 | | - user_input[CONF_RTSP_PORT] = None |
116 | | - user_input[CONF_MQTT_PREFIX] = None |
117 | | - user_input[CONF_TOPIC_STATUS] = None |
118 | | - user_input[CONF_TOPIC_MOTION_DETECTION] = None |
119 | | - user_input[CONF_TOPIC_SOUND_DETECTION] = None |
120 | | - user_input[CONF_TOPIC_MOTION_DETECTION_IMAGE] = None |
121 | | - |
122 | | - return self.async_create_entry( |
123 | | - title=user_input[CONF_NAME], |
124 | | - data=user_input |
125 | | - ) |
| 154 | + return await self.async_process_input(user_input) |
126 | 155 |
|
127 | 156 | return self.async_show_form( |
128 | 157 | step_id="user", |
129 | 158 | data_schema=vol.Schema(DATA_SCHEMA), |
130 | 159 | errors=errors, |
131 | 160 | ) |
| 161 | + |
| 162 | + async def async_step_zeroconf( |
| 163 | + self, discovery_info: ZeroconfServiceInfo |
| 164 | + ) -> ConfigFlowResult: |
| 165 | + """Handle a flow initialized by zeroconf discovery.""" |
| 166 | + errors = {} |
| 167 | + |
| 168 | + host = discovery_info.host |
| 169 | + if discovery_info.port is not None and discovery_info.port != 0: |
| 170 | + port = discovery_info.port |
| 171 | + else: |
| 172 | + port = 80 |
| 173 | + |
| 174 | + hostname = discovery_info.hostname |
| 175 | + name = discovery_info.name.split(".", 1)[0] |
| 176 | + mac = discovery_info.properties["mac"] |
| 177 | + |
| 178 | + mac = format_mac(mac) |
| 179 | + |
| 180 | + if hostname is None: |
| 181 | + return self.async_abort(reason="not_yi-hack_device") |
| 182 | + |
| 183 | + _LOGGER.info("Device with MAC %s already exists, update IP to %s and port to %s", mac, host, port) |
| 184 | + await self.async_set_unique_id(mac) |
| 185 | + self._abort_if_unique_id_configured(updates={CONF_HOST: host, CONF_PORT: port}) |
| 186 | + |
| 187 | + self.context.update( |
| 188 | + { |
| 189 | + "title_placeholders": {"name": name}, |
| 190 | + } |
| 191 | + ) |
| 192 | + |
| 193 | + self.connection_data.update( |
| 194 | + { |
| 195 | + CONF_HOST: host, |
| 196 | + CONF_PORT: port, |
| 197 | + CONF_USERNAME: "", |
| 198 | + CONF_PASSWORD: "", |
| 199 | + CONF_EXTRA_ARGUMENTS: DEFAULT_EXTRA_ARGUMENTS, |
| 200 | + CONF_BOOST_SPEAKER: "auto", |
| 201 | + } |
| 202 | + ) |
| 203 | + |
| 204 | + return await self.async_step_zeroconf_confirm() |
| 205 | + |
| 206 | + async def async_step_zeroconf_confirm( |
| 207 | + self, user_input: dict[str, Any] | None = None |
| 208 | + ) -> ConfigFlowResult: |
| 209 | + """Handle a confirmation flow initiated by zeroconf.""" |
| 210 | + errors = {} |
| 211 | + |
| 212 | + if user_input is not None: |
| 213 | + user_input[CONF_HOST] = self.connection_data[CONF_HOST] |
| 214 | + user_input[CONF_PORT] = self.connection_data[CONF_PORT] |
| 215 | + |
| 216 | + return await self.async_process_input(user_input) |
| 217 | + |
| 218 | + return self.async_show_form( |
| 219 | + step_id="zeroconf_confirm", |
| 220 | + data_schema=vol.Schema(DATA_SCHEMA_ZC), |
| 221 | + errors=errors, |
| 222 | + ) |
0 commit comments