In the Home APIs for iOS, observing changes to state in the home is made
possible through the use of the
[Combine framework](https://developer.apple.com/documentation/combine)
in Swift. Observing changes in structures, rooms, device metadata, and
device state in the Home APIs can be done with any API using
[`HomeDevice`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/HomeDevice). This is done
by *subscribing* to *publishers* exposing values from asynchronous events.

When any item in a collection is added, deleted, or modified, the latest
snapshot of the collection is returned.

It is up to the developer to deduce the specific changes by comparing this
snapshot with an older copy. The `id` field provided for
[each parent object type in the Home APIs](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/HomeDevice#id)
can be used for this purpose.

## How to unwrap optional objects

When an object is optional, use `if let` or `guard` to safely unwrap
the object. Don't use `!` because `!` should never be used unless the developer
knows for certain that the object cannot be nil.

The input parameter `structureID` in the example is
defined as `String?`, which means it's a string that is optional and has
the possibility of being nil. This function finds the `Structure` object
based on the input in `structureID`.

Here's how we recommend you handle optional objects:

    func structure(structureID: String?) -> Structure? {
        guard let structureID else { return nil }
        return structures.first { $0.id == structureID }
      }

## How to use publishers

What follows are some basic examples of working with publishers in the Home
APIs. For the following examples, an instance of the `Home`
must be created.

    var home: Home?

Before accessing collections, make sure to unwrap them since they're optional:

`guard let home else { return nil }`

### Track changes to a structure

The following changes to a structure trigger this collection:

- Structure [name](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/Structure#name)

    home.structures().batched()
      .compactMap { $0.first(where: { $0.id = myStructureID} }
      .removeDuplicates()
      .sink { structure in
        if let structure = structure {
          print("Structure \(structure.id) updated to \(structure.name)")
        }
      }

### Track changes to a specific device

The following changes to a device trigger this collection:

- [Connectivity state](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Enums/ConnectivityState)
- Device [name](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/HomeDevice#name)
- [Room](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/HomeDevice#roomid) membership
- The set of supported [traits](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/DeviceType#traits)
- The set of supported [types](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/HomeDevice#types)

    home.devices().batched()
      .compactMap { deviceList -> HomeDevice? in
        deviceList.filter { $0.name == "Bedroom Lamp"}.first
      }
      .removeDuplicates()
      .sink { lampDevice in
        if lampDevice != nil {
          print("The bedroom lamp has changed!")
        }
      }

### Track changes to a specific trait on a device

Use any trait supported by the device and the Home APIs. For a full list, see
[`Trait`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/Trait).

    device.types.subscribe(OnOffLightDeviceType.self)
      .compactMap { $0.matterTraits.onOffTrait }
      .removeDuplicates()
      .sink { onOffState in
        if let onOffState = onOffState {
          print("Got new state update: \(onOffState.onOff)")
        }
      }

### Track changes to a specific type on a device

The following changes to a device type trigger this collection:

- Changes to any trait within the [generated device
  type](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/DeviceType#traits)

Use any Matter device type supported by the Home APIs. For
a full list, see
[`DeviceType`](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Protocols/DeviceType).

    device.types.subscribe(DimmableLightDeviceType.self)
      .compactMap { $0.matterTraits.levelControlTrait }
      .removeDuplicates()
      .sink { dimmableLightDevice in
        if let dimmableLightDevice = dimmableLightDevice
          print("Got new state update! \(levelControlTrait.currentLevel)")
        }
      }

### Track changes to a room in a structure

The following changes to a room trigger this collection:

- Room [name](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/Room#name)
- [Structure](https://developers.home.google.com/reference/swift/GoogleHomeSDK/Structs/Room#structureid) membership

To track when devices are added or removed from a room, use the [`devices()`
query](https://developers.home.google.com/apis/ios/device/monitor#device).

    home.rooms().batched()
      .sink { rooms in
        print("Got a new updated set of rooms!")
        for room in rooms {
          print("Got room #\(room.name)")
        }
      }

## Subscribe to events

In the Home APIs, events are used to detect changes in the state of
a device.

> [!IMPORTANT]
> **Important:** Ideally, a developer could rely on devices and apps to send the expected events according to the Matter specification. However, device manufacturers aren't required to send Matter events or support optional event operation sources. For example, a door lock might emit `LockOperationErrorEvent` for remote app operations (`REMOTE`), but not for physical keypad operations (`KEYPAD`). In these cases, to monitor device state, you should use the techniques described in [Access devices and device metadata for iOS](https://developers.home.google.com/apis/eap/ios/device).

    self.cancellable = self.device.types.subscribe(FanDeviceType.self)
      .receive(on: DispatchQueue.main)
      .catch { error in
        // Error getting FanDeviceType
        return Empty<FanDeviceType, Never>().eraseToAnyPublisher()
      }
      .sink { [weak self] fanDeviceType in
        self?.fanDeviceType = fanDeviceType
        self?.updateTileInfo()
      }
    }