1 """Config flow for ZHA."""
3 from __future__
import annotations
6 from contextlib
import suppress
10 import serial.tools.list_ports
11 from serial.tools.list_ports_common
import ListPortInfo
12 import voluptuous
as vol
13 from zha.application.const
import RadioType
15 from zigpy.config
import CONF_DEVICE, CONF_DEVICE_PATH
40 from .const
import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN
41 from .radio_manager
import (
43 HARDWARE_DISCOVERY_SCHEMA,
49 CONF_MANUAL_PATH =
"Enter Manually"
50 SUPPORTED_PORT_SETTINGS = (
54 DECONZ_DOMAIN =
"deconz"
56 FORMATION_STRATEGY =
"formation_strategy"
57 FORMATION_FORM_NEW_NETWORK =
"form_new_network"
58 FORMATION_FORM_INITIAL_NETWORK =
"form_initial_network"
59 FORMATION_REUSE_SETTINGS =
"reuse_settings"
60 FORMATION_CHOOSE_AUTOMATIC_BACKUP =
"choose_automatic_backup"
61 FORMATION_UPLOAD_MANUAL_BACKUP =
"upload_manual_backup"
63 CHOOSE_AUTOMATIC_BACKUP =
"choose_automatic_backup"
64 OVERWRITE_COORDINATOR_IEEE =
"overwrite_coordinator_ieee"
66 OPTIONS_INTENT_MIGRATE =
"intent_migrate"
67 OPTIONS_INTENT_RECONFIGURE =
"intent_reconfigure"
69 UPLOADED_BACKUP_FILE =
"uploaded_backup_file"
71 REPAIR_MY_URL =
"https://my.home-assistant.io/redirect/repairs/"
73 LEGACY_ZEROCONF_PORT = 6638
74 LEGACY_ZEROCONF_ESPHOME_API_PORT = 6053
76 ZEROCONF_SERVICE_TYPE =
"_zigbee-coordinator._tcp.local."
77 ZEROCONF_PROPERTIES_SCHEMA = vol.Schema(
79 vol.Required(
"radio_type"): vol.All(str, vol.In([t.name
for t
in RadioType])),
80 vol.Required(
"serial_number"): str,
82 extra=vol.ALLOW_EXTRA,
87 backup: zigpy.backups.NetworkBackup, *, pan_ids: bool =
True
89 """Format network backup info into a short piece of text."""
91 return dt_util.as_local(backup.backup_time).strftime(
"%c")
95 f
"{str(backup.network_info.pan_id)[2:]}"
97 f
":{str(backup.network_info.extended_pan_id).replace(':', '')}"
100 return f
"{dt_util.as_local(backup.backup_time).strftime('%c')} ({identifier})"
104 """List all serial ports, including the Yellow radio and the multi-PAN addon."""
105 ports = await hass.async_add_executor_job(serial.tools.list_ports.comports)
109 yellow_hardware.async_info(hass)
110 except HomeAssistantError:
113 yellow_radio = next(p
for p
in ports
if p.device ==
"/dev/ttyAMA1")
114 yellow_radio.description =
"Yellow Zigbee module"
115 yellow_radio.manufacturer =
"Nabu Casa"
120 await silabs_multiprotocol_addon.get_multiprotocol_addon_manager(hass)
124 addon_info = await multipan_manager.async_get_addon_info()
125 except (AddonError, KeyError):
128 if addon_info
is not None and addon_info.state != AddonState.NOT_INSTALLED:
129 addon_port = ListPortInfo(
130 device=silabs_multiprotocol_addon.get_zigbee_socket(),
131 skip_link_detection=
True,
134 addon_port.description =
"Multiprotocol add-on"
135 addon_port.manufacturer =
"Nabu Casa"
136 ports.append(addon_port)
142 """Mixin for common ZHA flow steps and forms."""
148 """Initialize flow instance."""
155 def hass(self) -> HomeAssistant:
157 return self.
_hass_hass
160 def hass(self, hass: HomeAssistant) ->
None:
162 self.
_hass_hass = hass
166 """Create a config entry with the current flow state."""
167 assert self.
_radio_mgr_radio_mgr.radio_type
is not None
168 assert self.
_radio_mgr_radio_mgr.device_path
is not None
169 assert self.
_radio_mgr_radio_mgr.device_settings
is not None
171 device_settings = self.
_radio_mgr_radio_mgr.device_settings.copy()
172 device_settings[CONF_DEVICE_PATH] = await self.
hasshasshass.async_add_executor_job(
173 usb.get_serial_by_id, self.
_radio_mgr_radio_mgr.device_path
180 CONF_RADIO_TYPE: self.
_radio_mgr_radio_mgr.radio_type.name,
185 self, user_input: dict[str, Any] |
None =
None
186 ) -> ConfigFlowResult:
187 """Choose a serial port."""
190 f
"{p}{', s/n: ' + p.serial_number if p.serial_number else ''}"
191 + (f
" - {p.manufacturer}" if p.manufacturer
else "")
195 if not list_of_ports:
198 list_of_ports.append(CONF_MANUAL_PATH)
200 if user_input
is not None:
201 user_selection = user_input[CONF_DEVICE_PATH]
203 if user_selection == CONF_MANUAL_PATH:
206 port = ports[list_of_ports.index(user_selection)]
207 self.
_radio_mgr_radio_mgr.device_path = port.device
209 probe_result = await self.
_radio_mgr_radio_mgr.detect_radio_type()
210 if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED:
212 reason=
"wrong_firmware_installed",
213 description_placeholders={
"repair_url": REPAIR_MY_URL},
215 if probe_result == ProbeResult.PROBING_FAILED:
220 f
"{port.description}{', s/n: ' + port.serial_number if port.serial_number else ''}"
221 f
" - {port.manufacturer}"
229 default_port: vol.Undefined | str = vol.UNDEFINED
231 if self.
_radio_mgr_radio_mgr.device_path
is not None:
232 for description, port
in zip(list_of_ports, ports, strict=
False):
233 if port.device == self.
_radio_mgr_radio_mgr.device_path:
234 default_port = description
237 default_port = CONF_MANUAL_PATH
241 vol.Required(CONF_DEVICE_PATH, default=default_port): vol.In(
246 return self.
async_show_formasync_show_form(step_id=
"choose_serial_port", data_schema=schema)
249 self, user_input: dict[str, Any] |
None =
None
250 ) -> ConfigFlowResult:
251 """Manually select the radio type."""
252 if user_input
is not None:
253 self.
_radio_mgr_radio_mgr.radio_type = RadioType.get_by_description(
254 user_input[CONF_RADIO_TYPE]
259 default: vol.Undefined | str = vol.UNDEFINED
261 if self.
_radio_mgr_radio_mgr.radio_type
is not None:
262 default = self.
_radio_mgr_radio_mgr.radio_type.description
265 vol.Required(CONF_RADIO_TYPE, default=default): vol.In(RadioType.list())
269 step_id=
"manual_pick_radio_type",
270 data_schema=vol.Schema(schema),
274 self, user_input: dict[str, Any] |
None =
None
275 ) -> ConfigFlowResult:
276 """Enter port settings specific for this type of radio."""
277 assert self.
_radio_mgr_radio_mgr.radio_type
is not None
280 if user_input
is not None:
281 self.
_title_title = user_input[CONF_DEVICE_PATH]
282 self.
_radio_mgr_radio_mgr.device_path = user_input[CONF_DEVICE_PATH]
283 self.
_radio_mgr_radio_mgr.device_settings = user_input.copy()
285 if await self.
_radio_mgr_radio_mgr.radio_type.controller.probe(user_input):
288 errors[
"base"] =
"cannot_connect"
292 CONF_DEVICE_PATH, default=self.
_radio_mgr_radio_mgr.device_path
or vol.UNDEFINED
296 source = self.context.
get(
"source")
300 )
in DEVICE_SCHEMA.schema.items():
301 if param
not in SUPPORTED_PORT_SETTINGS:
304 if source == SOURCE_ZEROCONF
and param == CONF_BAUDRATE:
306 param = vol.Required(CONF_BAUDRATE, default=value)
308 self.
_radio_mgr_radio_mgr.device_settings
is not None
309 and param
in self.
_radio_mgr_radio_mgr.device_settings
311 param = vol.Required(
312 str(param), default=self.
_radio_mgr_radio_mgr.device_settings[param]
315 schema[param] = value
318 step_id=
"manual_port_config",
319 data_schema=vol.Schema(schema),
324 self, user_input: dict[str, Any] |
None =
None
325 ) -> ConfigFlowResult:
326 """Add a warning step to dissuade the use of deprecated radios."""
327 assert self.
_radio_mgr_radio_mgr.radio_type
is not None
330 if user_input
is not None or self.
_radio_mgr_radio_mgr.radio_type
in RECOMMENDED_RADIOS:
334 step_id=
"verify_radio",
335 description_placeholders={
336 CONF_NAME: self.
_radio_mgr_radio_mgr.radio_type.description,
337 "docs_recommended_adapters_url": (
338 "https://www.home-assistant.io/integrations/zha/#recommended-zigbee-radio-adapters-and-modules"
344 self, user_input: dict[str, Any] |
None =
None
345 ) -> ConfigFlowResult:
346 """Choose how to deal with the current radio's settings."""
347 await self.
_radio_mgr_radio_mgr.async_load_network_settings()
354 self.
_radio_mgr_radio_mgr.current_settings
is None
356 not backup.is_compatible_with(self.
_radio_mgr_radio_mgr.current_settings)
357 for backup
in self.
_radio_mgr_radio_mgr.backups
360 strategies.append(CHOOSE_AUTOMATIC_BACKUP)
362 if self.
_radio_mgr_radio_mgr.current_settings
is not None:
363 strategies.append(FORMATION_REUSE_SETTINGS)
365 strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP)
368 if self.
_radio_mgr_radio_mgr.current_settings
is None:
369 strategies.append(FORMATION_FORM_INITIAL_NETWORK)
371 strategies.append(FORMATION_FORM_NEW_NETWORK)
374 if not onboarding.async_is_onboarded(self.
hasshasshass)
and set(strategies) == {
375 FORMATION_UPLOAD_MANUAL_BACKUP,
376 FORMATION_FORM_INITIAL_NETWORK,
382 step_id=
"choose_formation_strategy",
383 menu_options=strategies,
387 self, user_input: dict[str, Any] |
None =
None
388 ) -> ConfigFlowResult:
389 """Reuse the existing network settings on the stick."""
393 self, user_input: dict[str, Any] |
None =
None
394 ) -> ConfigFlowResult:
395 """Form an initial network."""
400 self, user_input: dict[str, Any] |
None =
None
401 ) -> ConfigFlowResult:
402 """Form a brand-new network."""
403 await self.
_radio_mgr_radio_mgr.async_form_network()
407 self, uploaded_file_id: str
408 ) -> zigpy.backups.NetworkBackup:
409 """Read and parse an uploaded backup JSON file."""
411 contents = file_path.read_text()
413 return zigpy.backups.NetworkBackup.from_dict(json.loads(contents))
416 self, user_input: dict[str, Any] |
None =
None
417 ) -> ConfigFlowResult:
418 """Upload and restore a coordinator backup JSON file."""
421 if user_input
is not None:
423 self.
_radio_mgr_radio_mgr.chosen_backup = await self.
hasshasshass.async_add_executor_job(
427 errors[
"base"] =
"invalid_backup_json"
432 step_id=
"upload_manual_backup",
433 data_schema=vol.Schema(
444 self, user_input: dict[str, Any] |
None =
None
445 ) -> ConfigFlowResult:
446 """Choose an automatic backup."""
451 for backup
in self.
_radio_mgr_radio_mgr.backups
455 num_backups_on_date = collections.Counter(
456 backup.backup_time.date()
for backup
in self.
_radio_mgr_radio_mgr.backups
460 backup, pan_ids=(num_backups_on_date[backup.backup_time.date()] > 1)
462 for backup
in self.
_radio_mgr_radio_mgr.backups
465 if user_input
is not None:
466 index = choices.index(user_input[CHOOSE_AUTOMATIC_BACKUP])
472 step_id=
"choose_automatic_backup",
473 data_schema=vol.Schema(
475 vol.Required(CHOOSE_AUTOMATIC_BACKUP, default=choices[0]): vol.In(
483 self, user_input: dict[str, Any] |
None =
None
484 ) -> ConfigFlowResult:
485 """Confirm restore for EZSP radios that require permanent IEEE writes."""
486 call_step_2 = await self.
_radio_mgr_radio_mgr.async_restore_backup_step_1()
490 if user_input
is not None:
491 await self.
_radio_mgr_radio_mgr.async_restore_backup_step_2(
492 user_input[OVERWRITE_COORDINATOR_IEEE]
497 step_id=
"maybe_confirm_ezsp_restore",
498 data_schema=vol.Schema(
499 {vol.Required(OVERWRITE_COORDINATOR_IEEE, default=
True): bool}
505 """Handle a config flow."""
510 self, unique_id: str, device_path: str
512 """Set the flow's unique ID and update the device path in an ignored flow."""
515 if not current_entry:
518 if current_entry.source != SOURCE_IGNORE:
525 **current_entry.data.get(CONF_DEVICE, {}),
526 CONF_DEVICE_PATH: device_path,
534 config_entry: ConfigEntry,
536 """Create the options flow."""
540 self, user_input: dict[str, Any] |
None =
None
541 ) -> ConfigFlowResult:
542 """Handle a ZHA config flow start."""
549 self, user_input: dict[str, Any] |
None =
None
550 ) -> ConfigFlowResult:
551 """Confirm a discovery."""
560 if user_input
is not None or not onboarding.async_is_onboarded(self.
hasshasshass):
562 if self.
_radio_mgr_radio_mgr.radio_type
is None:
563 probe_result = await self.
_radio_mgr_radio_mgr.detect_radio_type()
565 probe_result = ProbeResult.RADIO_TYPE_DETECTED
567 if probe_result == ProbeResult.WRONG_FIRMWARE_INSTALLED:
569 reason=
"wrong_firmware_installed",
570 description_placeholders={
"repair_url": REPAIR_MY_URL},
572 if probe_result == ProbeResult.PROBING_FAILED:
578 if self.
_radio_mgr_radio_mgr.device_settings
is None:
585 description_placeholders={CONF_NAME: self.
_title_title_title},
589 self, discovery_info: usb.UsbServiceInfo
590 ) -> ConfigFlowResult:
591 """Handle usb discovery."""
592 vid = discovery_info.vid
593 pid = discovery_info.pid
594 serial_number = discovery_info.serial_number
595 manufacturer = discovery_info.manufacturer
596 description = discovery_info.description
597 dev_path = discovery_info.device
600 unique_id=f
"{vid}:{pid}_{serial_number}_{manufacturer}_{description}",
601 device_path=dev_path,
606 if self.
hasshasshass.config_entries.flow.async_progress_by_handler(DECONZ_DOMAIN):
608 for entry
in self.
hasshasshass.config_entries.async_entries(DECONZ_DOMAIN):
609 if entry.source != SOURCE_IGNORE:
612 self.
_radio_mgr_radio_mgr.device_path = dev_path
613 self.
_title_title_title = description
or usb.human_readable_device_name(
621 self.context[
"title_placeholders"] = {CONF_NAME: self.
_title_title_title}
625 self, discovery_info: zeroconf.ZeroconfServiceInfo
626 ) -> ConfigFlowResult:
627 """Handle zeroconf discovery."""
630 if discovery_info.type != ZEROCONF_SERVICE_TYPE:
631 port = discovery_info.port
or LEGACY_ZEROCONF_PORT
632 name = discovery_info.name
635 if "tube" in name
and port == LEGACY_ZEROCONF_ESPHOME_API_PORT:
636 port = LEGACY_ZEROCONF_PORT
639 if "radio_type" in discovery_info.properties:
640 radio_type = discovery_info.properties[
"radio_type"]
641 elif "efr32" in name:
642 radio_type = RadioType.ezsp.name
643 elif "zigate" in name:
644 radio_type = RadioType.zigate.name
646 radio_type = RadioType.znp.name
648 fallback_title = name.split(
"._", 1)[0]
649 title = discovery_info.properties.get(
"name", fallback_title)
652 ip_address=discovery_info.ip_address,
653 ip_addresses=discovery_info.ip_addresses,
655 hostname=discovery_info.hostname,
656 type=ZEROCONF_SERVICE_TYPE,
657 name=f
"{title}.{ZEROCONF_SERVICE_TYPE}",
659 "radio_type": radio_type,
661 "serial_number": discovery_info.hostname.removesuffix(
".local."),
670 radio_type = self.
_radio_mgr_radio_mgr.parse_radio_type(discovery_props[
"radio_type"])
671 device_path = f
"socket://{discovery_info.host}:{discovery_info.port}"
672 title = discovery_info.name.removesuffix(f
".{ZEROCONF_SERVICE_TYPE}")
675 unique_id=discovery_props[
"serial_number"],
676 device_path=device_path,
679 self.context[
"title_placeholders"] = {CONF_NAME: title}
681 self.
_radio_mgr_radio_mgr.device_path = device_path
682 self.
_radio_mgr_radio_mgr.radio_type = radio_type
684 CONF_DEVICE_PATH: device_path,
685 CONF_BAUDRATE: 115200,
686 CONF_FLOW_CONTROL:
None,
692 self, data: dict[str, Any] |
None =
None
693 ) -> ConfigFlowResult:
694 """Handle hardware flow."""
700 name = discovery_data[
"name"]
701 radio_type = self.
_radio_mgr_radio_mgr.parse_radio_type(discovery_data[
"radio_type"])
702 device_settings = discovery_data[
"port"]
703 device_path = device_settings[CONF_DEVICE_PATH]
706 unique_id=f
"{name}_{radio_type.name}_{device_path}",
707 device_path=device_path,
711 self.
_radio_mgr_radio_mgr.radio_type = radio_type
712 self.
_radio_mgr_radio_mgr.device_path = device_path
713 self.
_radio_mgr_radio_mgr.device_settings = device_settings
714 self.context[
"title_placeholders"] = {CONF_NAME: name}
720 """Handle an options flow."""
722 def __init__(self, config_entry: ConfigEntry) ->
None:
723 """Initialize options flow."""
725 self.
_radio_mgr_radio_mgr.device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
726 self.
_radio_mgr_radio_mgr.device_settings = config_entry.data[CONF_DEVICE]
727 self.
_radio_mgr_radio_mgr.radio_type = RadioType[config_entry.data[CONF_RADIO_TYPE]]
731 self, user_input: dict[str, Any] |
None =
None
732 ) -> ConfigFlowResult:
733 """Launch the options flow."""
734 if user_input
is not None:
736 with suppress(OperationNotAllowed):
744 self, user_input: dict[str, Any] |
None =
None
745 ) -> ConfigFlowResult:
746 """Confirm if we are migrating adapters or just re-configuring."""
749 step_id=
"prompt_migrate_or_reconfigure",
751 OPTIONS_INTENT_RECONFIGURE,
752 OPTIONS_INTENT_MIGRATE,
757 self, user_input: dict[str, Any] |
None =
None
758 ) -> ConfigFlowResult:
759 """Virtual step for when the user is reconfiguring the integration."""
763 self, user_input: dict[str, Any] |
None =
None
764 ) -> ConfigFlowResult:
765 """Confirm the user wants to reset their current radio."""
767 if user_input
is not None:
768 await self.
_radio_mgr_radio_mgr.async_reset_adapter()
775 self, user_input: dict[str, Any] |
None =
None
776 ) -> ConfigFlowResult:
777 """Instruct the user to unplug the current radio, if possible."""
779 if user_input
is not None:
786 """Re-implementation of the base flow's final step to update the config."""
787 device_settings = self.
_radio_mgr_radio_mgr.device_settings.copy()
788 device_settings[CONF_DEVICE_PATH] = await self.
hasshasshass.async_add_executor_job(
789 usb.get_serial_by_id, self.
_radio_mgr_radio_mgr.device_path
793 self.
hasshasshass.config_entries.async_update_entry(
796 CONF_DEVICE: device_settings,
797 CONF_RADIO_TYPE: self.
_radio_mgr_radio_mgr.radio_type.name,
809 """Maybe reload ZHA if the flow is aborted."""
811 ConfigEntryState.SETUP_ERROR,
812 ConfigEntryState.NOT_LOADED,
816 self.
hasshasshass.async_create_task(
ConfigFlowResult _async_create_radio_entry(self)
ConfigFlowResult async_step_verify_radio(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_manual_port_config(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_maybe_confirm_ezsp_restore(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_reuse_settings(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_choose_formation_strategy(self, dict[str, Any]|None user_input=None)
None hass(self, HomeAssistant hass)
ConfigFlowResult async_step_upload_manual_backup(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_choose_serial_port(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_form_new_network(self, dict[str, Any]|None user_input=None)
zigpy.backups.NetworkBackup _parse_uploaded_backup(self, str uploaded_file_id)
ConfigFlowResult async_step_choose_automatic_backup(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_manual_pick_radio_type(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_form_initial_network(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_confirm(self, dict[str, Any]|None user_input=None)
OptionsFlow async_get_options_flow(ConfigEntry config_entry)
ConfigFlowResult async_step_hardware(self, dict[str, Any]|None data=None)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_usb(self, usb.UsbServiceInfo discovery_info)
None _set_unique_id_and_update_ignored_flow(self, str unique_id, str device_path)
ConfigFlowResult async_step_zeroconf(self, zeroconf.ZeroconfServiceInfo discovery_info)
ConfigFlowResult async_step_prompt_migrate_or_reconfigure(self, dict[str, Any]|None user_input=None)
def _async_create_radio_entry(self)
ConfigFlowResult async_step_intent_reconfigure(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
ConfigFlowResult async_step_instruct_unplug(self, dict[str, Any]|None user_input=None)
None __init__(self, ConfigEntry config_entry)
ConfigFlowResult async_step_intent_migrate(self, dict[str, Any]|None user_input=None)
None _abort_if_unique_id_configured(self, dict[str, Any]|None updates=None, bool reload_on_update=True, *str error="already_configured")
None _set_confirm_only(self)
ConfigEntry|None async_set_unique_id(self, str|None unique_id=None, *bool raise_on_progress=True)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=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)
bool show_advanced_options(self)
_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_menu(self, *str|None step_id=None, Container[str] menu_options, Mapping[str, str]|None description_placeholders=None)
_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)
web.Response get(self, web.Request request, str config_key)
Iterator[Path] process_uploaded_file(HomeAssistant hass, str file_id)
bool is_hassio(HomeAssistant hass)
ZEROCONF_PROPERTIES_SCHEMA
str _format_backup_choice(zigpy.backups.NetworkBackup backup, *bool pan_ids=True)
list[ListPortInfo] list_serial_ports(HomeAssistant hass)
HARDWARE_DISCOVERY_SCHEMA