1 """Config flow for Z-Wave JS integration."""
3 from __future__
import annotations
5 from abc
import ABC, abstractmethod
11 from serial.tools
import list_ports
12 import voluptuous
as vol
13 from zwave_js_server.version
import VersionInfo, get_server_version
25 ConfigEntriesFlowManager,
44 from .
import disconnect_client
45 from .addon
import get_addon_manager
49 CONF_ADDON_EMULATE_HARDWARE,
51 CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
52 CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
53 CONF_ADDON_NETWORK_KEY,
54 CONF_ADDON_S0_LEGACY_KEY,
55 CONF_ADDON_S2_ACCESS_CONTROL_KEY,
56 CONF_ADDON_S2_AUTHENTICATED_KEY,
57 CONF_ADDON_S2_UNAUTHENTICATED_KEY,
58 CONF_INTEGRATION_CREATED_ADDON,
59 CONF_LR_S2_ACCESS_CONTROL_KEY,
60 CONF_LR_S2_AUTHENTICATED_KEY,
62 CONF_S2_ACCESS_CONTROL_KEY,
63 CONF_S2_AUTHENTICATED_KEY,
64 CONF_S2_UNAUTHENTICATED_KEY,
70 _LOGGER = logging.getLogger(__name__)
72 DEFAULT_URL =
"ws://localhost:3000"
75 ADDON_SETUP_TIMEOUT = 5
76 ADDON_SETUP_TIMEOUT_ROUNDS = 40
77 CONF_EMULATE_HARDWARE =
"emulate_hardware"
78 CONF_LOG_LEVEL =
"log_level"
79 SERVER_VERSION_TIMEOUT = 10
89 ADDON_USER_INPUT_MAP = {
90 CONF_ADDON_DEVICE: CONF_USB_PATH,
91 CONF_ADDON_S0_LEGACY_KEY: CONF_S0_LEGACY_KEY,
92 CONF_ADDON_S2_ACCESS_CONTROL_KEY: CONF_S2_ACCESS_CONTROL_KEY,
93 CONF_ADDON_S2_AUTHENTICATED_KEY: CONF_S2_AUTHENTICATED_KEY,
94 CONF_ADDON_S2_UNAUTHENTICATED_KEY: CONF_S2_UNAUTHENTICATED_KEY,
95 CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY: CONF_LR_S2_ACCESS_CONTROL_KEY,
96 CONF_ADDON_LR_S2_AUTHENTICATED_KEY: CONF_LR_S2_AUTHENTICATED_KEY,
97 CONF_ADDON_LOG_LEVEL: CONF_LOG_LEVEL,
98 CONF_ADDON_EMULATE_HARDWARE: CONF_EMULATE_HARDWARE,
101 ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=
True): bool})
105 """Return a schema for the manual step."""
106 default_url = user_input.get(CONF_URL, DEFAULT_URL)
107 return vol.Schema({vol.Required(CONF_URL, default=default_url): str})
111 """Return a schema for the on Supervisor step."""
112 default_use_addon = user_input[CONF_USE_ADDON]
113 return vol.Schema({vol.Optional(CONF_USE_ADDON, default=default_use_addon): bool})
117 """Validate if the user input allows us to connect."""
118 ws_address = user_input[CONF_URL]
120 if not ws_address.startswith((
"ws://",
"wss://")):
125 except CannotConnect
as err:
130 """Return Z-Wave JS version info."""
132 async
with asyncio.timeout(SERVER_VERSION_TIMEOUT):
133 version_info: VersionInfo = await get_server_version(
136 except (TimeoutError, aiohttp.ClientError)
as err:
139 _LOGGER.debug(
"Failed to connect to Z-Wave JS server: %s", err)
140 raise CannotConnect
from err
146 """Return a dict of USB ports and their friendly names."""
147 ports = list_ports.comports()
148 port_descriptions = {}
150 vid: str |
None =
None
151 pid: str |
None =
None
152 if port.vid
is not None and port.pid
is not None:
153 usb_device = usb.usb_device_from_port(port)
156 dev_path = usb.get_serial_by_id(port.device)
157 human_name = usb.human_readable_device_name(
165 port_descriptions[dev_path] = human_name
166 return port_descriptions
170 """Return a dict of USB ports and their friendly names."""
171 return await hass.async_add_executor_job(get_usb_ports)
175 """Represent the base config flow for Z-Wave JS."""
178 """Set up flow instance."""
179 self.s0_legacy_key: str |
None =
None
180 self.s2_access_control_key: str |
None =
None
181 self.s2_authenticated_key: str |
None =
None
182 self.s2_unauthenticated_key: str |
None =
None
183 self.lr_s2_access_control_key: str |
None =
None
184 self.lr_s2_authenticated_key: str |
None =
None
185 self.usb_path: str |
None =
None
187 self.restart_addon: bool =
False
190 self.
install_taskinstall_task: asyncio.Task |
None =
None
191 self.
start_taskstart_task: asyncio.Task |
None =
None
192 self.
version_infoversion_info: VersionInfo |
None =
None
196 def flow_manager(self) -> FlowManager[ConfigFlowContext, ConfigFlowResult]:
197 """Return the flow manager of the flow."""
200 self, user_input: dict[str, Any] |
None =
None
201 ) -> ConfigFlowResult:
202 """Install Z-Wave JS add-on."""
203 if not self.install_task:
208 step_id=
"install_addon",
209 progress_action=
"install_addon",
215 except AddonError
as err:
226 self, user_input: dict[str, Any] |
None =
None
227 ) -> ConfigFlowResult:
228 """Add-on installation failed."""
229 return self.
async_abortasync_abort(reason=
"addon_install_failed")
232 self, user_input: dict[str, Any] |
None =
None
233 ) -> ConfigFlowResult:
234 """Start Z-Wave JS add-on."""
240 step_id=
"start_addon",
241 progress_action=
"start_addon",
247 except (CannotConnect, AddonError, AbortFlow)
as err:
256 self, user_input: dict[str, Any] |
None =
None
257 ) -> ConfigFlowResult:
258 """Add-on start failed."""
259 return self.
async_abortasync_abort(reason=
"addon_start_failed")
262 """Start the Z-Wave JS add-on."""
265 if self.restart_addon:
266 await addon_manager.async_schedule_restart_addon()
268 await addon_manager.async_schedule_start_addon()
270 for _
in range(ADDON_SETUP_TIMEOUT_ROUNDS):
271 await asyncio.sleep(ADDON_SETUP_TIMEOUT)
276 f
"ws://{discovery_info['host']}:{discovery_info['port']}"
281 except (AbortFlow, CannotConnect)
as err:
283 "Add-on not ready yet, waiting %s seconds: %s",
290 raise CannotConnect(
"Failed to start Z-Wave JS add-on: timeout")
294 self, user_input: dict[str, Any] |
None =
None
295 ) -> ConfigFlowResult:
296 """Ask for config for Z-Wave JS add-on."""
300 self, user_input: dict[str, Any] |
None =
None
301 ) -> ConfigFlowResult:
302 """Prepare info needed to complete the config entry.
304 Get add-on discovery info and server version info.
305 Set unique id and abort if already configured.
309 """Return and cache Z-Wave JS add-on info."""
312 addon_info: AddonInfo = await addon_manager.async_get_addon_info()
313 except AddonError
as err:
315 raise AbortFlow(
"addon_info_failed")
from err
320 """Set Z-Wave JS add-on config."""
323 await addon_manager.async_set_addon_options(config)
324 except AddonError
as err:
326 raise AbortFlow(
"addon_set_config_failed")
from err
329 """Install the Z-Wave JS add-on."""
331 await addon_manager.async_schedule_install_addon()
334 """Return add-on discovery info."""
337 discovery_info_config = await addon_manager.async_get_addon_discovery_info()
338 except AddonError
as err:
340 raise AbortFlow(
"addon_get_discovery_info_failed")
from err
342 return discovery_info_config
346 """Handle a config flow for Z-Wave JS."""
353 """Set up flow instance."""
360 """Return the correct flow manager."""
361 return self.hass.config_entries.flow
366 config_entry: ConfigEntry,
367 ) -> OptionsFlowHandler:
368 """Return the options flow."""
372 self, user_input: dict[str, Any] |
None =
None
373 ) -> ConfigFlowResult:
374 """Handle the initial step."""
381 self, discovery_info: ZeroconfServiceInfo
382 ) -> ConfigFlowResult:
383 """Handle zeroconf discovery."""
384 home_id =
str(discovery_info.properties[
"homeId"])
388 self.context.
update({
"title_placeholders": {CONF_NAME: home_id}})
392 self, user_input: dict |
None =
None
393 ) -> ConfigFlowResult:
394 """Confirm the setup."""
395 if user_input
is not None:
401 step_id=
"zeroconf_confirm",
402 description_placeholders={
409 self, discovery_info: usb.UsbServiceInfo
410 ) -> ConfigFlowResult:
411 """Handle USB Discovery."""
419 vid = discovery_info.vid
420 pid = discovery_info.pid
421 serial_number = discovery_info.serial_number
422 manufacturer = discovery_info.manufacturer
423 description = discovery_info.description
425 if vid ==
"10C4" and pid ==
"EA60" and description
and "2652" in description:
429 if addon_info.state
not in (AddonState.NOT_INSTALLED, AddonState.NOT_RUNNING):
433 f
"{vid}:{pid}_{serial_number}_{manufacturer}_{description}"
436 dev_path = discovery_info.device
438 self.
_title_title = usb.human_readable_device_name(
446 self.context[
"title_placeholders"] = {
447 CONF_NAME: self.
_title_title.split(
" - ")[0].strip()
452 self, user_input: dict[str, Any] |
None =
None
453 ) -> ConfigFlowResult:
454 """Handle USB Discovery confirmation."""
455 if user_input
is None:
457 step_id=
"usb_confirm",
458 description_placeholders={CONF_NAME: self.
_title_title},
466 self, user_input: dict[str, Any] |
None =
None
467 ) -> ConfigFlowResult:
468 """Handle a manual configuration."""
469 if user_input
is None:
478 except InvalidInput
as err:
479 errors[
"base"] = err.error
481 _LOGGER.exception(
"Unexpected exception")
482 errors[
"base"] =
"unknown"
485 str(version_info.home_id), raise_on_progress=
False
492 CONF_USE_ADDON:
False,
493 CONF_INTEGRATION_CREATED_ADDON:
False,
504 self, discovery_info: HassioServiceInfo
505 ) -> ConfigFlowResult:
506 """Receive configuration from add-on discovery info.
508 This flow is triggered by the Z-Wave JS add-on.
513 if discovery_info.slug != ADDON_SLUG:
517 f
"ws://{discovery_info.config['host']}:{discovery_info.config['port']}"
521 except CannotConnect:
530 self, user_input: dict[str, Any] |
None =
None
531 ) -> ConfigFlowResult:
532 """Confirm the add-on discovery."""
533 if user_input
is not None:
535 user_input={CONF_USE_ADDON:
True}
541 self, user_input: dict[str, Any] |
None =
None
542 ) -> ConfigFlowResult:
543 """Handle logic when on Supervisor host."""
544 if user_input
is None:
546 step_id=
"on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA
548 if not user_input[CONF_USE_ADDON]:
555 if addon_info.state == AddonState.RUNNING:
556 addon_config = addon_info.options
557 self.
usb_pathusb_path = addon_config[CONF_ADDON_DEVICE]
558 self.
s0_legacy_keys0_legacy_key = addon_config.get(CONF_ADDON_S0_LEGACY_KEY,
"")
560 CONF_ADDON_S2_ACCESS_CONTROL_KEY,
""
563 CONF_ADDON_S2_AUTHENTICATED_KEY,
""
566 CONF_ADDON_S2_UNAUTHENTICATED_KEY,
""
569 CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY,
""
572 CONF_ADDON_LR_S2_AUTHENTICATED_KEY,
""
576 if addon_info.state == AddonState.NOT_RUNNING:
582 self, user_input: dict[str, Any] |
None =
None
583 ) -> ConfigFlowResult:
584 """Ask for config for Z-Wave JS add-on."""
586 addon_config = addon_info.options
588 if user_input
is not None:
589 self.
s0_legacy_keys0_legacy_key = user_input[CONF_S0_LEGACY_KEY]
596 self.
usb_pathusb_path = user_input[CONF_USB_PATH]
600 CONF_ADDON_DEVICE: self.
usb_pathusb_path,
609 if new_addon_config != addon_config:
614 usb_path = self.
usb_pathusb_path
or addon_config.get(CONF_ADDON_DEVICE)
or ""
615 s0_legacy_key = addon_config.get(
616 CONF_ADDON_S0_LEGACY_KEY, self.
s0_legacy_keys0_legacy_key
or ""
618 s2_access_control_key = addon_config.get(
621 s2_authenticated_key = addon_config.get(
624 s2_unauthenticated_key = addon_config.get(
627 lr_s2_access_control_key = addon_config.get(
630 lr_s2_authenticated_key = addon_config.get(
634 schema: VolDictType = {
635 vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str,
637 CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key
639 vol.Optional(CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key): str,
641 CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key
644 CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key
647 CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key
654 vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports),
658 data_schema = vol.Schema(schema)
663 self, user_input: dict[str, Any] |
None =
None
664 ) -> ConfigFlowResult:
665 """Prepare info needed to complete the config entry.
667 Get add-on discovery info and server version info.
668 Set unique id and abort if already configured.
674 if not self.
unique_idunique_id
or self.context[
"source"] == SOURCE_USB:
680 except CannotConnect
as err:
681 raise AbortFlow(
"cannot_connect")
from err
690 CONF_USB_PATH: self.
usb_pathusb_path,
703 """Return a config entry for the flow."""
706 self.hass.config_entries.flow.async_abort(progress[
"flow_id"])
712 CONF_USB_PATH: self.
usb_pathusb_path,
726 """Handle an options flow for Z-Wave JS."""
729 """Set up the options flow."""
736 """Return the correct flow manager."""
737 return self.hass.config_entries.options
741 """Update the config entry with new data."""
745 self, user_input: dict[str, Any] |
None =
None
746 ) -> ConfigFlowResult:
747 """Manage the options."""
754 self, user_input: dict[str, Any] |
None =
None
755 ) -> ConfigFlowResult:
756 """Handle a manual configuration."""
757 if user_input
is None:
769 except InvalidInput
as err:
770 errors[
"base"] = err.error
772 _LOGGER.exception(
"Unexpected exception")
773 errors[
"base"] =
"unknown"
776 return self.
async_abortasync_abort(reason=
"different_device")
784 CONF_USE_ADDON:
False,
785 CONF_INTEGRATION_CREATED_ADDON:
False,
797 self, user_input: dict[str, Any] |
None =
None
798 ) -> ConfigFlowResult:
799 """Handle logic when on Supervisor host."""
800 if user_input
is None:
802 step_id=
"on_supervisor",
807 if not user_input[CONF_USE_ADDON]:
812 if addon_info.state == AddonState.NOT_INSTALLED:
818 self, user_input: dict[str, Any] |
None =
None
819 ) -> ConfigFlowResult:
820 """Ask for config for Z-Wave JS add-on."""
822 addon_config = addon_info.options
824 if user_input
is not None:
835 CONF_ADDON_DEVICE: self.
usb_pathusb_path,
842 CONF_ADDON_LOG_LEVEL: user_input[CONF_LOG_LEVEL],
843 CONF_ADDON_EMULATE_HARDWARE: user_input.get(
844 CONF_EMULATE_HARDWARE,
False
848 if new_addon_config != addon_config:
849 if addon_info.state == AddonState.RUNNING:
854 new_addon_config.pop(CONF_ADDON_NETWORK_KEY,
None)
857 if addon_info.state == AddonState.RUNNING
and not self.
restart_addonrestart_addon:
869 usb_path = addon_config.get(CONF_ADDON_DEVICE, self.
usb_pathusb_path
or "")
870 s0_legacy_key = addon_config.get(
871 CONF_ADDON_S0_LEGACY_KEY, self.
s0_legacy_keys0_legacy_key
or ""
873 s2_access_control_key = addon_config.get(
876 s2_authenticated_key = addon_config.get(
879 s2_unauthenticated_key = addon_config.get(
882 lr_s2_access_control_key = addon_config.get(
885 lr_s2_authenticated_key = addon_config.get(
888 log_level = addon_config.get(CONF_ADDON_LOG_LEVEL,
"info")
889 emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE,
False)
893 data_schema = vol.Schema(
895 vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports),
896 vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str,
898 CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key
901 CONF_S2_AUTHENTICATED_KEY, default=s2_authenticated_key
904 CONF_S2_UNAUTHENTICATED_KEY, default=s2_unauthenticated_key
907 CONF_LR_S2_ACCESS_CONTROL_KEY, default=lr_s2_access_control_key
910 CONF_LR_S2_AUTHENTICATED_KEY, default=lr_s2_authenticated_key
912 vol.Optional(CONF_LOG_LEVEL, default=log_level): vol.In(
915 vol.Optional(CONF_EMULATE_HARDWARE, default=emulate_hardware): bool,
919 return self.
async_show_formasync_show_form(step_id=
"configure_addon", data_schema=data_schema)
922 self, user_input: dict[str, Any] |
None =
None
923 ) -> ConfigFlowResult:
924 """Add-on start failed."""
928 self, user_input: dict[str, Any] |
None =
None
929 ) -> ConfigFlowResult:
930 """Prepare info needed to complete the config entry update.
932 Get add-on discovery info and server version info.
933 Check for same unique id and abort if not the same unique id.
950 except CannotConnect:
960 CONF_USB_PATH: self.
usb_pathusb_path,
967 CONF_USE_ADDON:
True,
976 """Abort the options flow.
978 If the add-on options have been changed, revert those and restart add-on.
983 "Failed to revert add-on options before aborting flow, reason: %s",
992 addon_config_input = {
993 ADDON_USER_INPUT_MAP[addon_key]: addon_val
995 if addon_key
in ADDON_USER_INPUT_MAP
997 _LOGGER.debug(
"Reverting add-on options, reason: %s", reason)
1002 """Indicate connection error."""
1005 class InvalidInput(HomeAssistantError):
1006 """Error to indicate input data is invalid."""
1009 """Initialize error."""
FlowManager[ConfigFlowContext, ConfigFlowResult] flow_manager(self)
ConfigFlowResult async_step_install_failed(self, dict[str, Any]|None user_input=None)
dict _async_get_addon_discovery_info(self)
ConfigFlowResult async_step_start_failed(self, dict[str, Any]|None user_input=None)
None _async_start_addon(self)
None _async_install_addon(self)
integration_created_addon
AddonInfo _async_get_addon_info(self)
ConfigFlowResult async_step_start_addon(self, dict[str, Any]|None user_input=None)
None _async_set_addon_config(self, dict config)
ConfigFlowResult async_step_install_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_configure_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_finish_addon_setup(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_configure_addon(self, dict[str, Any]|None user_input=None)
OptionsFlowManager flow_manager(self)
ConfigFlowResult async_step_manual(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_start_failed(self, dict[str, Any]|None user_input=None)
None _async_update_entry(self, dict[str, Any] data)
ConfigFlowResult async_step_on_supervisor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_finish_addon_setup(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_revert_addon_config(self, str reason)
OptionsFlowHandler async_get_options_flow(ConfigEntry config_entry)
ConfigEntriesFlowManager flow_manager(self)
ConfigFlowResult async_step_manual(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_finish_addon_setup(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_usb_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_hassio_confirm(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_on_supervisor(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_usb(self, usb.UsbServiceInfo discovery_info)
ConfigFlowResult async_step_configure_addon(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_zeroconf_confirm(self, dict|None user_input=None)
ConfigFlowResult _async_create_entry_from_vars(self)
ConfigFlowResult async_step_hassio(self, HassioServiceInfo discovery_info)
ConfigFlowResult async_step_zeroconf(self, ZeroconfServiceInfo discovery_info)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
list[ConfigFlowResult] _async_in_progress(self, bool include_uninitialized=False, dict[str, Any]|None match_context=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
ConfigEntry config_entry(self)
None config_entry(self, ConfigEntry value)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_show_progress(self, *str|None step_id=None, str progress_action, Mapping[str, str]|None description_placeholders=None, asyncio.Task[Any]|None progress_task=None)
_FlowResultT async_show_progress_done(self, *str next_step_id)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool is_hassio(HomeAssistant hass)
IssData update(pyiss.ISS iss)
AddonManager get_addon_manager(HomeAssistant hass, str slug)
vol.Schema get_on_supervisor_schema(dict[str, Any] user_input)
dict[str, str] get_usb_ports()
vol.Schema get_manual_schema(dict[str, Any] user_input)
VersionInfo async_get_version_info(HomeAssistant hass, str ws_address)
dict[str, str] async_get_usb_ports(HomeAssistant hass)
VersionInfo validate_input(HomeAssistant hass, dict user_input)
None disconnect_client(HomeAssistant hass, ConfigEntry entry)
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)