Esempio n. 1
0
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)
Esempio n. 2
0
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()
Esempio n. 3
0
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)))
Esempio n. 4
0
        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)
Esempio n. 5
0
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
Esempio n. 6
0
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
Esempio n. 7
0
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