async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" use_addon = entry.data.get(CONF_USE_ADDON) if use_addon: await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = await device_registry.async_get_registry(hass) ent_reg = entity_registry.async_get(hass) @callback def migrate_entity(platform: str, old_unique_id: str, new_unique_id: str) -> None: """Check if entity with old unique ID exists, and if so migrate it to new ID.""" if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id): LOGGER.debug( "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", entity_id, old_unique_id, new_unique_id, ) try: ent_reg.async_update_entity( entity_id, new_unique_id=new_unique_id, ) except ValueError: LOGGER.debug( ("Entity %s can't be migrated because the unique ID is taken. " "Cleaning it up since it is likely no longer valid."), entity_id, ) ent_reg.async_remove(entity_id)
async def async_setup_entry( # noqa: C901 opp: OpenPeerPower, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" use_addon = entry.data.get(CONF_USE_ADDON) if use_addon: await async_ensure_addon_running(opp, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(opp)) dev_reg = device_registry.async_get(opp) ent_reg = entity_registry.async_get(opp) entry_opp_data: dict = opp.data[DOMAIN].setdefault(entry.entry_id, {}) unsubscribe_callbacks: list[Callable] = [] entry_opp_data[DATA_CLIENT] = client entry_opp_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks entry_opp_data[DATA_PLATFORM_SETUP] = {} registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) async def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) platform_setup_tasks = entry_opp_data[DATA_PLATFORM_SETUP] # register (or update) node in device registry device = register_node_in_dev_reg(opp, entry, dev_reg, client, node) # We only want to create the defaultdict once, even on reinterviews if device.id not in registered_unique_ids: registered_unique_ids[device.id] = defaultdict(set) value_updates_disc_info = [] # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): platform = disc_info.platform # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value( opp, ent_reg, registered_unique_ids[device.id][platform], device, client, disc_info, ) if platform not in platform_setup_tasks: platform_setup_tasks[platform] = opp.async_create_task( opp.config_entries.async_forward_entry_setup( entry, platform)) await platform_setup_tasks[platform] LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send(opp, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info) # Capture discovery info for values we want to watch for updates if disc_info.assumed_state: value_updates_disc_info.append(disc_info) # add listener for value updated events if necessary if value_updates_disc_info: unsubscribe_callbacks.append( node.on( "value updated", lambda event: async_on_value_updated( value_updates_disc_info, event["value"]), )) # add listener for stateless node value notification events unsubscribe_callbacks.append( node.on( "value notification", lambda event: async_on_value_notification(event[ "value_notification"]), )) # add listener for stateless node notification events unsubscribe_callbacks.append( node.on( "notification", lambda event: async_on_notification(event["notification"]), )) async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: await async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) node.once( "ready", lambda event: opp.async_create_task( async_on_node_ready(event["node"])), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(opp, entry, dev_reg, client, node) @callback def async_on_node_removed(node: ZwaveNode) -> None: """Handle node removed event.""" # grab device in device registry attached to this node dev_id = get_device_id(client, node) device = dev_reg.async_get_device({dev_id}) # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id) # type: ignore registered_unique_ids.pop(device.id, None) # type: ignore @callback def async_on_value_notification(notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to opp.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) opp.bus.async_fire( ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_ENDPOINT: notification.endpoint, ATTR_DEVICE_ID: device.id, # type: ignore ATTR_COMMAND_CLASS: notification.command_class, ATTR_COMMAND_CLASS_NAME: notification.command_class_name, ATTR_LABEL: notification.metadata.label, ATTR_PROPERTY: notification.property_, ATTR_PROPERTY_NAME: notification.property_name, ATTR_PROPERTY_KEY: notification.property_key, ATTR_PROPERTY_KEY_NAME: notification.property_key_name, ATTR_VALUE: value, ATTR_VALUE_RAW: raw_value, }, ) @callback def async_on_notification( notification: EntryControlNotification | NotificationNotification, ) -> None: """Relay stateless notification events from Z-Wave nodes to opp.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) event_data = { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_DEVICE_ID: device.id, # type: ignore ATTR_COMMAND_CLASS: notification.command_class, } if isinstance(notification, EntryControlNotification): event_data.update({ ATTR_COMMAND_CLASS_NAME: "Entry Control", ATTR_EVENT_TYPE: notification.event_type, ATTR_DATA_TYPE: notification.data_type, ATTR_EVENT_DATA: notification.event_data, }) else: event_data.update({ ATTR_COMMAND_CLASS_NAME: "Notification", ATTR_LABEL: notification.label, ATTR_TYPE: notification.type_, ATTR_EVENT: notification.event, ATTR_EVENT_LABEL: notification.event_label, ATTR_PARAMETERS: notification.parameters, }) opp.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) @callback def async_on_value_updated( value_updates_disc_info: list[ZwaveDiscoveryInfo], value: Value) -> None: """Fire value updated event.""" # Get the discovery info for the value that was updated. If we can't # find the discovery info, we don't need to fire an event try: disc_info = next( disc_info for disc_info in value_updates_disc_info if disc_info.primary_value.value_id == value.value_id) except StopIteration: return device = dev_reg.async_get_device({get_device_id(client, value.node)}) unique_id = get_unique_id(client.driver.controller.home_id, disc_info.primary_value.value_id) entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) raw_value = value_ = value.value if value.metadata.states: value_ = value.metadata.states.get(str(value), value_) opp.bus.async_fire( ZWAVE_JS_VALUE_UPDATED_EVENT, { ATTR_NODE_ID: value.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_DEVICE_ID: device.id, # type: ignore ATTR_ENTITY_ID: entity_id, ATTR_COMMAND_CLASS: value.command_class, ATTR_COMMAND_CLASS_NAME: value.command_class_name, ATTR_ENDPOINT: value.endpoint, ATTR_PROPERTY: value.property_, ATTR_PROPERTY_NAME: value.property_name, ATTR_PROPERTY_KEY: value.property_key, ATTR_PROPERTY_KEY_NAME: value.property_key_name, ATTR_VALUE: value_, ATTR_VALUE_RAW: raw_value, }, ) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): await client.connect() except InvalidServerVersion as err: if not entry_opp_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): LOGGER.error("Invalid server version: %s", err) entry_opp_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True if use_addon: async_ensure_addon_updated(opp) raise ConfigEntryNotReady from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: if not entry_opp_data.get(DATA_CONNECT_FAILED_LOGGED): LOGGER.error("Failed to connect: %s", err) entry_opp_data[DATA_CONNECT_FAILED_LOGGED] = True raise ConfigEntryNotReady from err else: LOGGER.info("Connected to Zwave JS Server") entry_opp_data[DATA_CONNECT_FAILED_LOGGED] = False entry_opp_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False services = ZWaveServices(opp, ent_reg) services.async_register() # Set up websocket API async_register_api(opp) async def start_platforms() -> None: """Start platforms and perform discovery.""" driver_ready = asyncio.Event() async def handle_op_shutdown(event: Event) -> None: """Handle OPP shutdown.""" await disconnect_client(opp, entry, client, listen_task, platform_task) listen_task = asyncio.create_task( client_listen(opp, entry, client, driver_ready)) entry_opp_data[DATA_CLIENT_LISTEN_TASK] = listen_task unsubscribe_callbacks.append( opp.bus.async_listen(EVENT_OPENPEERPOWER_STOP, handle_op_shutdown)) try: await driver_ready.wait() except asyncio.CancelledError: LOGGER.debug("Cancelling start platforms") return LOGGER.info("Connection to Zwave JS Server initialized") # If opt in preference hasn't been specified yet, we do nothing, otherwise # we apply the preference if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): await async_enable_statistics(client) elif opted_in is False: await client.driver.async_disable_statistics()
async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" use_addon = entry.data.get(CONF_USE_ADDON) if use_addon: await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client entry_hass_data[DATA_PLATFORM_SETUP] = {} registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) discovered_value_ids: dict[str, set[str]] = defaultdict(set) @callback def remove_device(device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id) registered_unique_ids.pop(device.id, None) discovered_value_ids.pop(device.id, None) async def async_handle_discovery_info( device: device_registry.DeviceEntry, disc_info: ZwaveDiscoveryInfo, value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], ) -> None: """Handle discovery info and all dependent tasks.""" # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value( hass, ent_reg, registered_unique_ids[device.id][disc_info.platform], device, client, disc_info, ) platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] platform = disc_info.platform if platform not in platform_setup_tasks: platform_setup_tasks[platform] = hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform)) await platform_setup_tasks[platform] LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send(hass, f"{DOMAIN}_{entry.entry_id}_add_{platform}", disc_info) # If we don't need to watch for updates return early if not disc_info.assumed_state: return value_updates_disc_info[disc_info.primary_value.value_id] = disc_info # If this is the first time we found a value we want to watch for updates, # return early if len(value_updates_disc_info) != 1: return # add listener for value updated events entry.async_on_unload( disc_info.node.on( "value updated", lambda event: async_on_value_updated_fire_event( value_updates_disc_info, event["value"]), )) async def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) # register (or update) node in device registry device = register_node_in_dev_reg(hass, entry, dev_reg, client, node, remove_device) # We only want to create the defaultdict once, even on reinterviews if device.id not in registered_unique_ids: registered_unique_ids[device.id] = defaultdict(set) value_updates_disc_info: dict[str, ZwaveDiscoveryInfo] = {} # run discovery on all node values and create/update entities await asyncio.gather(*(async_handle_discovery_info( device, disc_info, value_updates_disc_info) for disc_info in async_discover_node_values( node, device, discovered_value_ids))) # add listeners to handle new values that get added later for event in ("value added", "value updated", "metadata updated"): entry.async_on_unload( node.on( event, lambda event: hass.async_create_task( async_on_value_added(value_updates_disc_info, event[ "value"])), )) # add listener for stateless node value notification events entry.async_on_unload( node.on( "value notification", lambda event: async_on_value_notification(event[ "value_notification"]), )) # add listener for stateless node notification events entry.async_on_unload( node.on( "notification", lambda event: async_on_notification(event["notification"]), )) async def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] # We need to set up the sensor platform if it hasn't already been setup in # order to create the node status sensor if SENSOR_DOMAIN not in platform_setup_tasks: platform_setup_tasks[SENSOR_DOMAIN] = hass.async_create_task( hass.config_entries.async_forward_entry_setup( entry, SENSOR_DOMAIN)) # This guard ensures that concurrent runs of this function all await the # platform setup task if not platform_setup_tasks[SENSOR_DOMAIN].done(): await platform_setup_tasks[SENSOR_DOMAIN] # Create a node status sensor for each device async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_node_status_sensor", node) # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: await async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) node.once( "ready", lambda event: hass.async_create_task( async_on_node_ready(event["node"])), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(hass, entry, dev_reg, client, node, remove_device) async def async_on_value_added(value_updates_disc_info: dict[ str, ZwaveDiscoveryInfo], value: Value) -> None: """Fire value updated event.""" # If node isn't ready or a device for this node doesn't already exist, we can # let the node ready event handler perform discovery. If a value has already # been processed, we don't need to do it again device_id = get_device_id(client, value.node) if (not value.node.ready or not (device := dev_reg.async_get_device({device_id})) or value.value_id in discovered_value_ids[device.id]): return LOGGER.debug("Processing node %s added value %s", value.node, value) await asyncio.gather(*(async_handle_discovery_info( device, disc_info, value_updates_disc_info) for disc_info in async_discover_single_value( value, device, discovered_value_ids)))
params[ATTR_SUGGESTED_AREA] = node.location device = dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) return device async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) entry_hass_data[DATA_CLIENT] = client entry_hass_data[DATA_PLATFORM_SETUP] = {} registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) discovered_value_ids: dict[str, set[str]] = defaultdict(set) @callback def remove_device(device: device_registry.DeviceEntry) -> None: """Remove device from registry.""" # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = await device_registry.async_get_registry(hass) @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) # register (or update) node in device registry register_node_in_dev_reg(hass, entry, dev_reg, client, node) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info) # add listener for stateless node value notification events node.on( "value notification", lambda event: async_on_value_notification(event[ "value_notification"]), ) # add listener for stateless node notification events node.on("notification", lambda event: async_on_notification(event["notification"])) @callback def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) node.once( "ready", lambda event: async_on_node_ready(event["node"]), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(hass, entry, dev_reg, client, node) @callback def async_on_node_removed(node: ZwaveNode) -> None: """Handle node removed event.""" # grab device in device registry attached to this node dev_id = get_device_id(client, node) device = dev_reg.async_get_device({dev_id}) # note: removal of entity registry is handled by core dev_reg.async_remove_device(device.id) @callback def async_on_value_notification(notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) hass.bus.async_fire( ZWAVE_JS_EVENT, { ATTR_TYPE: "value_notification", ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_ENDPOINT: notification.endpoint, ATTR_DEVICE_ID: device.id, ATTR_COMMAND_CLASS: notification.command_class, ATTR_COMMAND_CLASS_NAME: notification.command_class_name, ATTR_LABEL: notification.metadata.label, ATTR_PROPERTY_NAME: notification.property_name, ATTR_PROPERTY_KEY_NAME: notification.property_key_name, ATTR_VALUE: value, }, ) @callback def async_on_notification(notification: Notification) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) hass.bus.async_fire( ZWAVE_JS_EVENT, { ATTR_TYPE: "notification", ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_DEVICE_ID: device.id, ATTR_LABEL: notification.notification_label, ATTR_PARAMETERS: notification.parameters, }, ) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): await client.connect() except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady from err else: LOGGER.info("Connected to Zwave JS Server") unsubscribe_callbacks: List[Callable] = [] hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: client, DATA_UNSUBSCRIBE: unsubscribe_callbacks, } # Set up websocket API async_register_api(hass) async def start_platforms() -> None: """Start platforms and perform discovery.""" # wait until all required platforms are ready await asyncio.gather(*[ hass.config_entries.async_forward_entry_setup(entry, component) for component in PLATFORMS ]) driver_ready = asyncio.Event() async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await disconnect_client(hass, entry, client, listen_task, platform_task) listen_task = asyncio.create_task( client_listen(hass, entry, client, driver_ready)) hass.data[DOMAIN][ entry.entry_id][DATA_CLIENT_LISTEN_TASK] = listen_task unsubscribe_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown)) await driver_ready.wait() LOGGER.info("Connection to Zwave JS Server initialized") # Check for nodes that no longer exist and remove them stored_devices = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id) known_devices = [ dev_reg.async_get_device({get_device_id(client, node)}) for node in client.driver.controller.nodes.values() ] # Devices that are in the device registry that are not known by the controller can be removed for device in stored_devices: if device not in known_devices: dev_reg.async_remove_device(device.id) # run discovery on all ready nodes for node in client.driver.controller.nodes.values(): async_on_node_added(node) # listen for new nodes being added to the mesh client.driver.controller.on( "node added", lambda event: async_on_node_added(event["node"])) # listen for nodes being removed from the mesh # NOTE: This will not remove nodes that were removed when HA was not running client.driver.controller.on( "node removed", lambda event: async_on_node_removed(event["node"])) platform_task = hass.async_create_task(start_platforms()) hass.data[DOMAIN][entry.entry_id][DATA_START_PLATFORM_TASK] = platform_task return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" use_addon = entry.data.get(CONF_USE_ADDON) if use_addon: await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) # register (or update) node in device registry register_node_in_dev_reg(hass, entry, dev_reg, client, node) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) # This migration logic was added in 2021.3 to handle a breaking change to # the value_id format. Some time in the future, this call (as well as the # helper functions) can be removed. async_migrate_discovered_value(ent_reg, client, disc_info) async_dispatcher_send( hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info) # add listener for stateless node value notification events node.on( "value notification", lambda event: async_on_value_notification(event[ "value_notification"]), ) # add listener for stateless node notification events node.on("notification", lambda event: async_on_notification(event["notification"])) @callback def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready", node.node_id) node.once( "ready", lambda event: async_on_node_ready(event["node"]), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(hass, entry, dev_reg, client, node) @callback def async_on_node_removed(node: ZwaveNode) -> None: """Handle node removed event.""" # grab device in device registry attached to this node dev_id = get_device_id(client, node) device = dev_reg.async_get_device({dev_id}) # note: removal of entity registry entry is handled by core dev_reg.async_remove_device(device.id) # type: ignore @callback def async_on_value_notification(notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) raw_value = value = notification.value if notification.metadata.states: value = notification.metadata.states.get(str(value), value) hass.bus.async_fire( ZWAVE_JS_VALUE_NOTIFICATION_EVENT, { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_ENDPOINT: notification.endpoint, ATTR_DEVICE_ID: device.id, # type: ignore ATTR_COMMAND_CLASS: notification.command_class, ATTR_COMMAND_CLASS_NAME: notification.command_class_name, ATTR_LABEL: notification.metadata.label, ATTR_PROPERTY: notification.property_, ATTR_PROPERTY_NAME: notification.property_name, ATTR_PROPERTY_KEY: notification.property_key, ATTR_PROPERTY_KEY_NAME: notification.property_key_name, ATTR_VALUE: value, ATTR_VALUE_RAW: raw_value, }, ) @callback def async_on_notification( notification: EntryControlNotification | NotificationNotification, ) -> None: """Relay stateless notification events from Z-Wave nodes to hass.""" device = dev_reg.async_get_device( {get_device_id(client, notification.node)}) event_data = { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, ATTR_HOME_ID: client.driver.controller.home_id, ATTR_DEVICE_ID: device.id, # type: ignore ATTR_COMMAND_CLASS: notification.command_class, } if isinstance(notification, EntryControlNotification): event_data.update({ ATTR_COMMAND_CLASS_NAME: "Entry Control", ATTR_EVENT_TYPE: notification.event_type, ATTR_DATA_TYPE: notification.data_type, ATTR_EVENT_DATA: notification.event_data, }) else: event_data.update({ ATTR_COMMAND_CLASS_NAME: "Notification", ATTR_LABEL: notification.label, ATTR_TYPE: notification.type_, ATTR_EVENT: notification.event, ATTR_EVENT_LABEL: notification.event_label, ATTR_PARAMETERS: notification.parameters, }) hass.bus.async_fire(ZWAVE_JS_NOTIFICATION_EVENT, event_data) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): await client.connect() except InvalidServerVersion as err: if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): LOGGER.error("Invalid server version: %s", err) entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True if use_addon: async_ensure_addon_updated(hass) raise ConfigEntryNotReady from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED): LOGGER.error("Failed to connect: %s", err) entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True raise ConfigEntryNotReady from err else: LOGGER.info("Connected to Zwave JS Server") entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False unsubscribe_callbacks: list[Callable] = [] entry_hass_data[DATA_CLIENT] = client entry_hass_data[DATA_UNSUBSCRIBE] = unsubscribe_callbacks services = ZWaveServices(hass, ent_reg) services.async_register() # Set up websocket API async_register_api(hass) async def start_platforms() -> None: """Start platforms and perform discovery.""" # wait until all required platforms are ready await asyncio.gather(*[ hass.config_entries.async_forward_entry_setup(entry, platform) for platform in PLATFORMS ]) driver_ready = asyncio.Event() async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await disconnect_client(hass, entry, client, listen_task, platform_task) listen_task = asyncio.create_task( client_listen(hass, entry, client, driver_ready)) entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task unsubscribe_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown)) try: await driver_ready.wait() except asyncio.CancelledError: LOGGER.debug("Cancelling start platforms") return LOGGER.info("Connection to Zwave JS Server initialized") # Check for nodes that no longer exist and remove them stored_devices = device_registry.async_entries_for_config_entry( dev_reg, entry.entry_id) known_devices = [ dev_reg.async_get_device({get_device_id(client, node)}) for node in client.driver.controller.nodes.values() ] # Devices that are in the device registry that are not known by the controller can be removed for device in stored_devices: if device not in known_devices: dev_reg.async_remove_device(device.id) # run discovery on all ready nodes for node in client.driver.controller.nodes.values(): async_on_node_added(node) # listen for new nodes being added to the mesh client.driver.controller.on( "node added", lambda event: async_on_node_added(event["node"])) # listen for nodes being removed from the mesh # NOTE: This will not remove nodes that were removed when HA was not running client.driver.controller.on( "node removed", lambda event: async_on_node_removed(event["node"])) platform_task = hass.async_create_task(start_platforms()) entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) initialized = asyncio.Event() dev_reg = await device_registry.async_get_registry(hass) async def async_on_connect() -> None: """Handle websocket is (re)connected.""" LOGGER.info("Connected to Zwave JS Server") if initialized.is_set(): # update entity availability async_dispatcher_send(hass, f"{DOMAIN}_connection_state") async def async_on_disconnect() -> None: """Handle websocket is disconnected.""" LOGGER.info("Disconnected from Zwave JS Server") async_dispatcher_send(hass, f"{DOMAIN}_connection_state") async def async_on_initialized() -> None: """Handle initial full state received.""" LOGGER.info("Connection to Zwave JS Server initialized.") initialized.set() @callback def async_on_node_ready(node: ZwaveNode) -> None: """Handle node ready event.""" LOGGER.debug("Processing node %s", node) # register (or update) node in device registry register_node_in_dev_reg(hass, entry, dev_reg, client, node) # run discovery on all node values and create/update entities for disc_info in async_discover_values(node): LOGGER.debug("Discovered entity: %s", disc_info) async_dispatcher_send(hass, f"{DOMAIN}_add_{disc_info.platform}", disc_info) @callback def async_on_node_added(node: ZwaveNode) -> None: """Handle node added event.""" # we only want to run discovery when the node has reached ready state, # otherwise we'll have all kinds of missing info issues. if node.ready: async_on_node_ready(node) return # if node is not yet ready, register one-time callback for ready state LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) node.once( "ready", lambda event: async_on_node_ready(event["node"]), ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added register_node_in_dev_reg(hass, entry, dev_reg, client, node) async def handle_ha_shutdown(event: Event) -> None: """Handle HA shutdown.""" await client.disconnect() # register main event callbacks. unsubs = [ client.register_on_initialized(async_on_initialized), client.register_on_disconnect(async_on_disconnect), client.register_on_connect(async_on_connect), hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown), ] # connect and throw error if connection failed asyncio.create_task(client.connect()) try: async with timeout(CONNECT_TIMEOUT): await initialized.wait() except asyncio.TimeoutError as err: for unsub in unsubs: unsub() await client.disconnect() raise ConfigEntryNotReady from err hass.data[DOMAIN][entry.entry_id] = { DATA_CLIENT: client, DATA_UNSUBSCRIBE: unsubs, } # Set up websocket API async_register_api(hass) async def start_platforms() -> None: """Start platforms and perform discovery.""" # wait until all required platforms are ready await asyncio.gather( *[ hass.config_entries.async_forward_entry_setup(entry, component) for component in PLATFORMS ] ) # run discovery on all ready nodes for node in client.driver.controller.nodes.values(): async_on_node_added(node) # listen for new nodes being added to the mesh client.driver.controller.on( "node added", lambda event: async_on_node_added(event["node"]) ) hass.async_create_task(start_platforms()) return True