Skip to content

Commit a441815

Browse files
committed
Add autodiscovery feature
1 parent a6f54a6 commit a441815

File tree

8 files changed

+183
-85
lines changed

8 files changed

+183
-85
lines changed

custom_components/yi_hack/config_flow.py

Lines changed: 169 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@
44

55
import voluptuous as vol
66
from homeassistant import config_entries
7+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
8+
from homeassistant.components import zeroconf
79
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS
810
from homeassistant.const import (CONF_HOST, CONF_MAC, CONF_NAME, CONF_PASSWORD,
911
CONF_PORT, CONF_USERNAME)
1012
from homeassistant.helpers.device_registry import format_mac
13+
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
14+
15+
from typing import Any
1116

1217
from .common import get_status
1318
from .const import (ALLWINNER, ALLWINNER_R, ALLWINNERV2, ALLWINNERV2_R,
@@ -35,97 +40,183 @@
3540
): vol.In(["auto", "disabled", "x 2", "x 3", "x 4", "x 5"])
3641
}
3742

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+
3853
class YiHackFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
3954
"""Handle a yi-hack config flow."""
4055

4156
VERSION = 1
4257
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
4358

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:
45150
"""Handle a flow initiated by the user."""
46151
errors = {}
47152

48153
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)
126155

127156
return self.async_show_form(
128157
step_id="user",
129158
data_schema=vol.Schema(DATA_SCHEMA),
130159
errors=errors,
131160
)
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+
)

custom_components/yi_hack/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
"documentation": "https://github.com/roleoroleo/yi-hack_ha_integration",
88
"iot_class": "local_push",
99
"issue_tracker": "https://github.com/roleoroleo/yi-hack_ha_integration/issues",
10-
"version": "0.5.3"
10+
"version": "0.5.3",
11+
"zeroconf": ["_yi-hack._tcp.local."]
1112
}

custom_components/yi_hack/strings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
28-
"wrong_hack_version": "Unsupported hack version, please update your device"
28+
"wrong_hack_version": "Unsupported hack version, please update your device",
29+
"not_yi-hack_device": "This is not a Yi hack camera"
2930
}
3031
}
3132
}

custom_components/yi_hack/translations/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "Device is already configured",
28-
"wrong_hack_version": "Unsupported hack version, please update your device"
28+
"wrong_hack_version": "Unsupported hack version, please update your device",
29+
"not_yi-hack_device": "This is not a yi-hack camera"
2930
}
3031
}
3132
}

custom_components/yi_hack/translations/es.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "El dispositivo ya est\u00e1 configurado",
28-
"wrong_hack_version": "Versi\u00f3n de yi-hack no compatible, actualice su dispositivo"
28+
"wrong_hack_version": "Versi\u00f3n de yi-hack no compatible, actualice su dispositivo",
29+
"not_yi-hack_device": "Esta no es una c\u00e1mara yi-hack"
2930
}
3031
}
3132
}

custom_components/yi_hack/translations/it.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato",
28-
"wrong_hack_verions": "Versione di yi-hack non supportata, aggiorna il tuo dispositivo"
28+
"wrong_hack_version": "Versione di yi-hack non supportata, aggiorna il tuo dispositivo",
29+
"not_yi-hack_device": "Questa non \u00e8 una camera yi-hack"
2930
}
3031
}
3132
}

custom_components/yi_hack/translations/pl.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "Urządzenie już skonfigurowane",
28-
"wrong_hack_version": "Nieobsługiwana wersja hakerska, zaktualizuj swoje urządzenie"
28+
"wrong_hack_version": "Nieobsługiwana wersja hakerska, zaktualizuj swoje urządzenie",
29+
"not_yi-hack_device": "To nie jest aparat yi-hack"
2930
}
3031
}
3132
}

custom_components/yi_hack/translations/pt-BR.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"abort": {
2727
"already_configured": "O dispositivo já está configurado",
28-
"wrong_hack_version": "Versão de yi-hack não suportada, atualize seu dispositivo"
28+
"wrong_hack_version": "Versão de yi-hack não suportada, atualize seu dispositivo",
29+
"not_yi-hack_device": "Esta não é uma câmera yi-hack"
2930
}
3031
}
3132
}

0 commit comments

Comments
 (0)