Cómo controlar dispositivos en iOS

Esta guía continúa desde Accede a dispositivos y metadatos de dispositivos en iOS y presenta ejemplos adicionales de control y acceso de dispositivos.

Para usar tipos o atributos de dispositivos específicos, como el Matter OnOffTrait que se usa en muchos de los ejemplos que se muestran aquí, se deben importar:

import GoogleHomeSDK
import GoogleHomeTypes

Cómo verificar si un atributo admite un comando

Usa la función supports a nivel de la característica para verificar si un comando es compatible con un dispositivo en particular.

Por ejemplo, para verificar si un dispositivo admite el comando toggle de la característica On/Off, haz lo siguiente:

// Check if the OnOff trait supports the toggle command.
if onOffTraitTest.supportsToggleCommand {
  print("onOffTrait supports toggle command")
} else {
  print("onOffTrait does not support stateful toggle command")
}

Enviar un comando a un dispositivo

Enviar un comando es similar a leer un atributo de estado de un atributo. Para encender o apagar el dispositivo, usa el comando de activación OnOffTrait, que se define en el modelo de datos del ecosistema de Google Home como toggle(). Este método cambia onOff a false si es true, o a true si es false:

// Calling a command on a trait.
do {
  try await onOffTraitTest.toggle()
} catch let _ as HomeError {
  // Code for handling the exception
}

Los comandos pueden mostrar una excepción si se detecta un problema con el flujo de ejecución. Como desarrollador, debes usar un bloque do-catch para controlar estas excepciones de forma adecuada y mostrar información detallada a los usuarios en los casos en que los errores se puedan corregir. Las excepciones no controladas detendrán el tiempo de ejecución de la app y pueden provocar fallas en ella.

Como alternativa, usa los comandos off() o on() para establecer el estado de forma explícita:

do {
  try await onOffTraitTest.off()
  try await onOffTraitTest.on()
} catch let _ as HomeError {
  // Code for handling the exception
}

Después de enviar un comando para cambiar el estado, una vez que se complete, puedes leer el estado como se describe en Lee el estado de un dispositivo para controlarlo en tu app.

Envía un comando con parámetros

Algunos comandos pueden usar parámetros, como los de OnOffTrait o LevelControlTrait:

offWithEffect

// Turn off the light using the DyingLight effect.
do {
  try await onOffTraitTest.offWithEffect(
    effectIdentifier: Matter.OnOffTrait.EffectIdentifierEnum.dyingLight,
    effectVariant: 0
  )
} catch let _ as HomeError {
  // Code for handling the exception
}

moveToLevel

// Change the brightness of the light to 50%
do {
  try await levelControlTraitTest.moveToLevel(
    level: UInt8(127),
    transitionTime: 0,
    optionsMask: Matter.LevelControlTrait.OptionsBitmap(),
    optionsOverride: Matter.LevelControlTrait.OptionsBitmap()
  )
} catch let _ as HomeError {
  // Code for handling the exception
}

Verifica si un atributo admite un rasgo

Es posible que algunos dispositivos admitan un atributo Matter, pero no un atributo específico. Por ejemplo, un dispositivo Cloud-to-cloud que se asignó a Matter podría no admitir todos los atributos de Matter. Para controlar casos como estos, usa la propiedad isSupported a nivel de rasgo para verificar si el atributo es compatible con un dispositivo en particular.

Por ejemplo, para verificar si un dispositivo admite el atributo onOff de la característica On/Off, haz lo siguiente:

// Check if the OnOff trait supports the onOff attribute.
if onOffTrait.attributes.$onOff.isSupported {
  print("onOffTrait supports onOff state")
} else {
  print("onOffTrait is for a command only device!")
}

Algunos atributos pueden ser nulos en la especificación de Matter o en el esquema de smart home de Cloud-to-cloud. Para estos atributos, puedes determinar si un valor nil que devuelve el atributo se debe a que el dispositivo no informa ese valor o si el valor del atributo en realidad es nil, con isNullable además de isSupported:

// Check if a nullable attribute is set or is not supported.
if let deviceType = await device.types.get(OnOffLightDeviceType.self) {
  if let onOffTrait = deviceType.traits[Matter.OnOffTrait.self] {
    if onOffTrait.attributes.startUpOnOff == nil {
      if onOffTrait.attributes.$startUpOnOff.isSupported {
        print(
          "onOffTrait supports startUpOnOff and it is nil. Check the spec for the contextual meaning."
        )
      } else {
        print("onOffTrait does not support startUpOnOff!")
      }
    } else {
      print(
        "onOffTrait supports startUpOnOff and it is set to \(String(describing: onOffTrait.attributes.startUpOnOff))"
      )
    }
  }
}

Actualiza los atributos de los rasgos

Si quieres cambiar el valor de un atributo determinado y ninguno de los comandos del rasgo lo hace, es posible que el atributo admita que se establezca su valor de forma explícita.

La posibilidad de cambiar el valor de un atributo depende de dos factores:

  • ¿Se puede escribir en el atributo?
  • ¿Puede cambiar el valor del atributo como un efecto secundario de enviar un comando de rasgo?

La documentación de referencia de los rasgos y sus atributos proporciona esta información.

Por lo tanto, las combinaciones de propiedades que dictan cómo se puede cambiar el valor de un atributo son las siguientes:

  • Es de solo lectura y no se ve afectado por otros comandos. Esto significa que el valor del atributo no cambia. Por ejemplo, el atributo currentPosition de SwitchTrait.

  • Es de solo lectura y se ve afectado por otros comandos. Esto significa que la única forma en que el valor del atributo puede cambiar es como resultado del envío de un comando. Por ejemplo, el atributo currentLevel de LevelControlTrait es de solo lectura, pero su valor se puede mutar con comandos como moveToLevel.

  • Se puede escribir en él y no se ve afectado por otros comandos. Esto significa que puedes cambiar directamente el valor del atributo con la función update de la característica, pero no hay comandos que afecten el valor del atributo. Por ejemplo, el atributo WrongCodeEntryLimit de DoorLockTrait.

  • Se puede escribir en él y se ve afectado por otros comandos. Esto significa que puedes cambiar directamente el valor del atributo con la función update del rasgo, y el valor del atributo puede cambiar como resultado del envío de un comando. Por ejemplo, se puede escribir en el atributo occupiedCoolingSetpoint de ThermostatTrait, pero también se puede actualizar con el comando setpointRaiseLower.

Ejemplo de uso de la función de actualización para cambiar el valor de un atributo

En este ejemplo, se muestra cómo establecer de forma explícita el valor del atributo DoorLockTrait.wrongCodeEntryLimit.

Para establecer un valor de atributo, llama a la función update del rasgo y pásale una función de actualización que establezca el valor nuevo. Se recomienda verificar primero que el atributo sea compatible con el rasgo.

Por ejemplo:

if doorLockTraitTest.attributes.$wrongCodeEntryLimit.isSupported {
  let _ = try await doorLockTraitTest.update {
    $0.setWrongCodeEntryLimit(3)
  }
}

Controla un grupo de dispositivos

Además de tener un control detallado sobre cada miembro de un grupo mediante la iteración por sus dispositivos miembros, puedes controlar un DeviceGroup como una sola entidad. DeviceGroup cumple con el protocolo HasParts y expone las partes de DeviceType que admiten los dispositivos del grupo:

  • await group.part(_:) devuelve la primera instancia del DeviceType especificado si el grupo lo admite, o nil de lo contrario.
  • try await group.parts(_:) devuelve un array de todas las partes de nivel superior que coinciden con el DeviceType especificado.

Cuando invocas un comando de atributo en un tipo de dispositivo que forma parte de un DeviceGroup, el SDK retransmite el comando a todos los dispositivos miembros asociados del grupo que admiten esa capacidad, independientemente de su conectividad o transporte subyacentes (como Matter o Cloud-to-cloud).

Atributos de rasgos de partes de grupos y manejo de errores

La segmentación de una parte del grupo simplifica los casos comunes en los que deseas enviar un comando idempotente (como encender o apagar las luces, o establecer un nivel de brillo) en un grupo. Cuando trabajes con rasgos y comandos en una parte de DeviceGroup, ten en cuenta los siguientes comportamientos:

  • Los atributos de estado dinámicos (como el atributo onOff de OnOffTrait o el atributo currentLevel de LevelControlTrait) no suelen propagarse en el rasgo de una parte del grupo, ya que los dispositivos miembros de un grupo pueden estar en diferentes estados que no se pueden combinar en un solo valor. Sin embargo, los comandos admitidos (verificados con supports(_:), como .moveToLevelWithOnOff en una luz regulable) y los atributos de capacidad seleccionados (como colorTempPhysicalMinMireds y colorTempPhysicalMaxMireds en ColorControlTrait para luces de color) se propagan para que tu app pueda configurar los controles de la IU de forma adecuada.
  • No todos los comandos de los atributos de los miembros del grupo están disponibles en una parte del grupo. En particular, no están disponibles los comandos que devuelven valores.
  • Un comando en una parte de grupo no devuelve resultados individuales para cada comando de dispositivo miembro subyacente. Para inspeccionar los resultados de comandos individuales o llamar a comandos que devuelven valores, itera directamente a través de los dispositivos miembros del grupo.
  • Los comandos que se envían a una parte en un DeviceGroup se completan una vez que finaliza toda la distribución en los dispositivos miembros y se recopilan todos los éxitos o fallas de los comandos subyacentes. Un comando solo falla (arroja una HomeError) si falla para cada dispositivo miembro asociado o si todos los dispositivos de destino no están disponibles, lo que se muestra como una falla de un solo comando que coincide con la sintaxis de llamada de un solo comando. Si al menos un dispositivo miembro ejecuta el comando, el comando de grupo se completa sin arrojar un error.

Cómo enviar comandos a una parte de un grupo

Para encender todas las luces OnOffLightDeviceType de un grupo de dispositivos, di lo siguiente:

if let lightPart = await group.part(OnOffLightDeviceType.self),
   let onOffTrait = lightPart.matterTraits.onOffTrait,
   onOffTrait.supports(.on) {
  try await onOffTrait.on()
}

También puedes verificar varios tipos de dispositivos relacionados (por ejemplo, apagar cualquier tipo de dispositivo de luz del grupo que admita OnOffTrait):

func onOffTrait(in group: DeviceGroup) async -> Matter.OnOffTrait? {
  if let light = await group.part(OnOffLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(DimmableLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(ColorTemperatureLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  if let light = await group.part(ExtendedColorLightDeviceType.self) {
    return light.matterTraits.onOffTrait
  }
  return nil
}

if let trait = await onOffTrait(in: group) {
  try await trait.off()
}

Puedes verificar la compatibilidad de los comandos o leer los límites de capacidad de una parte del grupo antes de enviar un comando (por ejemplo, verificar .moveToLevelWithOnOff en una parte de luz regulable o leer colorTempPhysicalMinMireds y colorTempPhysicalMaxMireds en una parte de luz de temperatura de color):

if let dimmableLight = await group.part(DimmableLightDeviceType.self),
   let levelTrait = dimmableLight.matterTraits.levelControlTrait,
   levelTrait.supports(.moveToLevelWithOnOff) {
  try await levelTrait.moveToLevelWithOnOff(
    level: 127,
    transitionTime: nil,
    optionsMask: [],
    optionsOverride: []
  )
}

if let colorLight = await group.part(ColorTemperatureLightDeviceType.self),
   let colorTrait = colorLight.matterTraits.colorControlTrait,
   let minMireds = colorTrait.attributes.colorTempPhysicalMinMireds,
   let maxMireds = colorTrait.attributes.colorTempPhysicalMaxMireds {
  let midMireds = UInt16((UInt32(minMireds) + UInt32(maxMireds)) / 2)
  try await colorTrait.moveToColorTemperature(
    colorTemperatureMireds: midMireds,
    transitionTime: 0,
    optionsMask: [],
    optionsOverride: []
  )
}