Scene Creation

Last Updated on : 2024-01-29 06:43:03

This topic describes how to implement scene creation, including the conditions and actions.

Create conditions

Process

  1. Get the supported condition types by querying the list of conditions and display them to users as needed.

    ThingHomeSdk.getSceneServiceInstance().conditionService().getConditionAll(
                homeId, // The home ID
                true, "", object : IResultCallback<ConditionItemList?>() {
                    override fun onSuccess(result: ConditionItemList?) {
                // On top of the result, you can add local custom condition types.
                    }
                    override fun onError(errorCode: String?, errorMessage: String?) {
    
                    }
                })
    
  2. After users select a condition type, present the UI accordingly.

  3. When users complete the condition set, assemble the condition model of the scene using the builder provided by creating scene conditions.

An open source simplified example project is available to help you understand the above process.

When location changes

The example creates a geofence condition. It is recommended to use the geofencing component from Google or Huawei to monitor geofences and choose a suitable map for creating geofences. Alternatively, use Tuya’s geofence monitoring component. The following section will describe how to integrate with this component.

Specifically, get the radius, latitude, longitude, and address from the geofence drawing page. With these parameters, after users select geofenceType, create a condition of when the location changes.

val radius = 100
val latitude: Double = 30.30288959184809
val longitude: Double = 120.0640840491766
val address = "XX"
val geofenceType = GeofencingType.GEOFENCING_TYPE_ENTER.type
val geofenceConditionBuilder = GeofenceConditionBuilder(radius, latitude, longitude, address, geofenceType)
val conditionBase = geofenceConditionBuilder.build() as ConditionBase
val geoCondition = SceneCondition(conditionBase).apply {
    this.entityName = address
}

The geofencing components based on Google and Huawei have been integrated into the BizBundle SDK. You cannot use both of these components simultaneously. The geofencing based on Huawei takes precedence over the one based on Google.

Integrate Huawei geofencing component

  1. Follow the HMS Core documentation Configuring App Information in AppGallery Connect, Integrating the SDK, Configuring Obfuscation Scripts, and Optimizing the App Package Size to complete the preparations.

  2. Integrate the component.

    //If you have already included the following dependencies in the preparation steps, you can ignore them, but the version specified here prevails.
    implementation 'com.huawei.hms:maps:6.0.0.301'
    implementation 'com.huawei.hms:location:6.0.0.302'
    
    implementation "com.thingclips.smart:thingsmart:${sdk_version}"
    // BizBundle SDK components
    implementation "com.thingclips.smart:thingsmart-expansion-sdk:${expansion_version}"
    
    // If you already include the following dependencies, ignore them.
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.alibaba:fastjson:1.1.67.android'
    
  3. Write code.

    • It is recommended to declare the location permissions in the manifest file. After users select the condition of when location changes, check and request the necessary permissions accordingly. For more information, see Assigning App Permissions. If the app does not have location permissions, it may result in an API error.
    • Choose a suitable map for setting up geofences.
  4. Use the component.

    The section When Location Changes describes how to use the API. For the restrictions, see Geofence Restrictions and Devices Supporting Geofence.

Integrate Google geofencing component

  1. Integrate the component.

    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    
    implementation "com.thingclips.smart:thingsmart:${sdk_version}"
    // BizBundle SDK components
    implementation("com.thingclips.smart:thingsmart-expansion-sdk:${expansion_version}"){
      // Note: Exclude the Huawei-based geofencing component from the global dependencies to use the Google-based geofencing component.
      exclude group: "com.thingclips.smart", module: "thingsmart-geofence-huawei"
    }
    
    // If you already include the following dependencies, ignore them.
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.alibaba:fastjson:1.1.67.android'
    
  2. Configure the project.

    • Exclude the Huawei-based geofencing component from the global dependencies in the build.gradle.

       android{
           configurations.all{
               exclude group: "com.thingclips.smart", module: "thingsmart-geofence-huawei"
           }
       }
      
    • Configure the AndroidManifest.xml file.

      <!-- Replace ${GOOGLE_MAP_KEY} with your own Google Map key -->
      <meta-data
          android:name="com.google.android.geo.API_KEY"
          android:value="${GOOGLE_MAP_KEY}" />
      
  3. Write code.

    It is recommended to declare the location permissions in the manifest file. After users select the condition of when location changes, request the necessary permissions accordingly. For more information, see Request Location Permissions. If the app does not have location permissions, it may result in an API error.

  4. Use the component.

    The section When Location Changes describes how to use the API. For more information, see Create and Monitor Geofences and Use Best Practices for Geofencing.

Schedule

The example creates a schedule condition. It is recommended to provide a date and time picker component for users to select a time.

Get the time and date from the picker component, and the timeZoneId from either Calendar.getInstance().timeZone.id or TimeZone.getDefault().id. After users select the recurring type loops, create a schedule condition with the obtained parameters.

val conditionBase: ConditionBase = TimingConditionBuilder(
    timeZoneId,
    loops,
    time,
    date,
).build() as ConditionBase
val timerCondition = SceneCondition(conditionBase).apply {
    entityName = "Schedule"
    exprDisplay = time
}

When weather changes

The example creates a condition of when weather changes at a specific location. It is recommended to display the full envConds returned by querying the list of conditions. After users select a weather variable, provide them with a UI to input the desired value.

With the longitude and latitude of the location, you can get the value of cityId and city by calling the method of querying city information by longitude and latitude. Alternatively, query the list of cities and instruct users to select a city to get the city ID.

  • Temperature

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_TEMP,
        operator = "<", // User setting
        chooseValue = chooseValue, // User setting
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for weather type data in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Wind speed

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_WIND,
        operator = "<", // User setting
        chooseValue = chooseValue, // User setting
    ).setWindSpeedUnit(unit) // The unit of wind speed. This field takes the data of wind speed in the condition list, specifically property.property.unit.
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon //  The icon of the selected weather type. This field takes the value of newIcon for wind speed in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Humidity

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_HUMIDITY,
        operator = null,
        chooseValue = chooseValue, // User selection
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for humidity in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Weather

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_CONDITION,
        operator = null,
        chooseValue = chooseValue, // User selection
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for weather conditions in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • PM2.5

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_PM,
        operator = null,
        chooseValue = chooseValue, // User selection
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for PM2.5 in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Air quality

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_AQI,
        operator = null,
        chooseValue = chooseValue, // User selection
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for air quality in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Sunrise/Sunset

    val weatherConditionBuilder = WeatherConditionBuilder(
        cityId = cityId,
        cityName = city,
        entityType = 3,
        weatherType = WeatherType.WEATHER_TYPE_SUN,
        operator = null,
        chooseValue = chooseValue, // User selection
    )
    val conditionBase = weatherConditionBuilder.build() as ConditionBase
    val weatherCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for sunrise/sunset in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    
  • Time before or after sunrise/sunset

    val conditionBase: ConditionBase = SunRiseSetConditionBuilder(
        cityId,
        SunSetRiseRule.SunType.SUNSET, // User selection, SUNSET or SUNRISE
        20 // The time before or after sunrise/sunset
    ).build() as ConditionBase
    val sunRiseSetCondition = SceneCondition(conditionBase).apply {
        entityName = city
        iconUrl = weatherData.icon // The icon of the selected weather type. This field takes the value of newIcon for sunrise/sunset in the condition list.
        exprDisplay = exprDis // Visual representation of the conditional expressions.
    }
    

When device status changes

The example creates a condition of when device status changes. It is recommended to display the full list returned by querying the list of devices. After users select a device, get the list of DPs returned by querying the list of DPs for device conditions. Convert condition device DP model to DTO model to present it to users for setup. Then create a condition of when device status changes accordingly.

deviceConditionData is obtained by converting condition device DP model to DTO model. Its fields should be updated promptly after editing. For example, update ValueTypeData.value, ValueTypeData.operators, OtherTypeData.checked, and ConditionExtraInfo.timeWindow.

  • Common devices

    val builder = DeviceConditionBuilder(
        deviceId = deviceConditionData.deviceId,
        dpId = deviceConditionData.datapointId.toString(),
        entityType = deviceConditionData.entityType ?: CONDITION_TYPE_DEVICE,
        deviceConditionData = deviceConditionData, // After converting the condition device DP model to the DTO model, you can get the list of conditionData.
        chooseValue = chooseValue // User setting
    )
    
    val conditionBase = builder.build() as ConditionBase
    val deviceCondition = SceneCondition(conditionBase).apply {
        entityName = deviceBean.name // Device name
        entitySubIds = deviceConditionData.datapointId.toString() // Device DP ID
        iconUrl = deviceBean.getIconUrl()// Device icon
        exprDisplay = displayString // Visual representation of the conditional expressions.
    }
    
  • DP of temperature for common devices

    • The convertTemp map stores DP values in different temperature units.
    • originTempUnit specifies the temperature unit of the DP.
    • tempUnit specifies the app’s preferred temperature unit.
    • dpScale specifies the decimal places of the DP.
    val builder = DeviceConditionBuilder(
        deviceId = deviceConditionData.deviceId,
        dpId = deviceConditionData.datapointId.toString(),
        entityType = deviceConditionData.entityType ?: CONDITION_TYPE_DEVICE,
        deviceConditionData = deviceConditionData, // After converting the condition device DP model to the DTO model, you can get the list of conditionData.
        chooseValue = chooseValue // User setting
    )
    
    val originTempUnit = "celsius" // The temperature unit of the DP.
    val tempUnit = "fahrenheit" // The app's preferred temperature unit.
    val dpScale = 1
    val tempMap = mutableMapOf<String, Int>()
    tempMap[originTempUnit] = 120
    tempMap[tempUnit] = (120/10 * 1.8 + 32) * 10 // Convert Celsius to Fahrenheit
    
    builder.setConvertTemp(tempMap)
           .setTempUnit(tempUnit)
           .setOriginTempUnit(originTempUnit)
           .setDpScale(dpScale)
    
    val conditionBase = builder.build() as ConditionBase
    val deviceCondition = SceneCondition(conditionBase).apply {
        entityName = deviceBean.name // Device name
        entitySubIds = deviceConditionData.datapointId.toString() // Device DP ID
        iconUrl = deviceBean.getIconUrl()// Device icon
        exprDisplay = displayString // Visual representation of the conditional expressions.
    }
    
  • PIR devices

    val builder = DeviceConditionBuilder(
        deviceId = deviceConditionData.deviceId,
        dpId = deviceConditionData.datapointId.toString(),
        entityType = deviceConditionData.entityType ?: CONDITION_TYPE_DEVICE,
        deviceConditionData = deviceConditionData, // After converting the condition device DP model to the DTO model, you can get the list of conditionData.
        chooseValue = chooseValue // User setting
    ).setDelayTime(otherTypeData?.datapointKey ?: "") // Obtain from conditionData.otherTypeData.datapointKey
    
    val conditionBase = builder.build() as ConditionBase
    val deviceCondition = SceneCondition(conditionBase).apply {
        entityName = deviceBean.name // Device name
        entitySubIds = deviceConditionData.datapointId.toString() // Device DP ID
        iconUrl = deviceBean.getIconUrl()// Device icon
        exprDisplay = displayString // Visual representation of the conditional expressions.
    }
    
  • DP duration

    deviceConditionData.entityType = CONDITION_TYPE_WITH_TIME
    
    val builder = DeviceConditionBuilder(
        deviceId = deviceConditionData.deviceId,
        dpId = deviceConditionData.datapointId.toString(),
        entityType = deviceConditionData.entityType,
        deviceConditionData = deviceConditionData, // After converting the condition device DP model to the DTO model, you can get the list of conditionData.
        chooseValue = chooseValue // User setting
    ).setCalType(COND_TYPE_DURATION)
            .setTimeWindow(conditionData.extraInfo?.timeWindow ?: 0)
    
    val conditionBase = builder.build() as ConditionBase
    val deviceCondition = SceneCondition(conditionBase).apply {
        entityName = deviceBean.name // Device name
        entitySubIds = deviceConditionData.datapointId.toString() // Device DP ID
        iconUrl = deviceBean.getIconUrl()// Device icon
        exprDisplay = displayString // Visual representation of the conditional expressions.
    }
    

When home members come home

The example creates a condition of when home members come home. It is recommended to display the full list of door locks in the home. After users select a lock, display the full list of home members for selection.

devConds is obtained by querying the list of conditions.

val membersString = "zhangsan,lisi" // Assemble the names of the selected home member, separated by commas.
val memberIds = "123,345" // Assemble the IDs of the selected home member, separated by commas.
val conditionBase = DeviceConditionBuilder(
    deviceId = deviceId ?: "", // ID of the door lock
    dpId = devConds?.find { it.entityType == ConditionEntityType.LOCK_MEMBER_GO_HOME.type }?.entitySubId ?: "",
    entityType = ConditionEntityType.LOCK_MEMBER_GO_HOME.type,
    deviceConditionData = null,
    chooseValue = memberIds
).setMembers(membersString)
      .build() as ConditionBase

val lockCondition = SceneCondition(conditionBase).apply {
    entityName = membersString
    exprDisplay = display // Visual representation of the conditional expressions.
}

Create actions

Process

  1. Display the list of action types to choose from.

  2. After users select an action type, present the UI accordingly.

  3. When users complete the action set, assemble the action model of the scene using the builder provided by creating scene actions.

Delay actions

The example creates a delay action. It is recommended to provide a date and time picker component for users to select a time.

Convert the hour, minute, and second obtained from the picker component into minutes and seconds. Then create a delay action accordingly.

val minutes = 62
val seconds = 20
val actionBase: ActionBase = DelayActionBuilder(minutes, seconds).build() as ActionBase
val delayAction = SceneAction(actionBase).apply {
    entityName = "Delay Action"
}

Send notifications

The example creates an action of sending a notification to the specified channel. You can provide the UI for setup based on the notification types.

notifyType is based on the user’s selection. The constants for message center, SMS, and phone call are ACTION_TYPE_MESSAGE, ACTION_TYPE_SMS, and ACTION_TYPE_PHONE respectively, which are defined in package: com.thingclips.smart.scene.model.constant.*. With these parameters, create an action of sending a notification to the specified channel.

SMS and phone calls are value-added services that can be purchased on the Tuya IoT Development Platform before they can be used. This guide only describes how to create the data model for the notification actions.

  • Message center

    val actionBase: ActionBase = NotifyActionBuilder(ACTION_TYPE_MESSAGE).build() as ActionBase
    val notifyAction = SceneAction(actionBase).apply {
        entityName = "Message Center"
    }
    
  • SMS message

    You can prompt users to subscribe to the value-added services by specifying the text with actionDisplayNew. To display the subtitle of the notification action, extract the content from actionDisplayNew.

    Example:

    val unableTip = "The prompt indicating SMS service is not available"
    val actionBase: ActionBase = NotifyActionBuilder(ACTION_TYPE_SMS).build() as ActionBase
    val notifyAction = SceneAction(actionBase).apply {
        entityName = "SMS Notification"
        actionDisplayNew = mutableMapOf(
            "package_has_expired" to listOf(unableTip)
        )
    }
    
  • Phone call

    val unableTip = "The prompt indicating phone notification service is not available"
    val actionBase: ActionBase = NotifyActionBuilder(ACTION_TYPE_PHONE).build() as ActionBase
    val notifyAction = SceneAction(actionBase).apply {
        entityName = "Phone Call"
        actionDisplayNew = mutableMapOf(
            "voice_package_has_expired" to listOf(unableTip)
        )
    }
    

Select smart scenes

The example displays the Tap-to-Run scenes and automations returned by querying the lightweight list of scenes, and then creates a scene action after the user’s selection. You can retrieve the scene data from the local data source, provided that you have cached and updated the list. Provide the UI for setup based on the scene types.

You can get the scene ID linkageRuleId from the selected scene. linkageOperator is determined by the scene type selected. The constants for Tap-to-Run, automation enabled, and automation disabled are ACTION_TYPE_TRIGGER, ACTION_TYPE_ENABLE_AUTOMATION, and ACTION_TYPE_DISABLE_AUTOMATION respectively. With these parameters, create a scene action.

  • Tap-to-Run action

    val actionExecutor = ACTION_TYPE_TRIGGER
    val actionBase: ActionBase = LinkageRuleActionBuilder(checkedItem.id, actionExecutor).build() as ActionBase // checkedItem.id is the ID of Tap-to-Run
    SceneAction(actionBase).apply {
        entityName = checkedItem.name // Name of Tap-to-Run
    }
    
  • Automation action

    val actionExecutor = ACTION_TYPE_ENABLE_AUTOMATION
    val actionBase: ActionBase = LinkageRuleActionBuilder(checkedItem.id, actionExecutor).build() as ActionBase
    // checkedItem.id is the ID of automation
    SceneAction(actionBase).apply {
       entityName = checkedItem.name // Name of automation
    }
    

Control single device

The example creates an action of controlling a specific DP of a device. It is recommended to display the full list of devices and groups returned by querying the list of action devices. After users select a device or group, get the DPs returned by querying the list of DPs for the action device or group. Convert action device DP model to DTO model to present it to users for setup. Then create an action of controlling a device.

deviceActionDetailBean is obtained by converting action device DP model to DTO model. selDpValue represents the selected DP value, while selDpDisplayValue is its visual representation.

  • Common devices

    val builder = DeviceActionBuilder(deviceId, deviceActionDetailBean, selDpValue, selDpDisplayValue ?: "")
    val actionBase: ActionBase = builder.build() as ActionBase
    SceneAction(actionBase).apply {
        actionDisplayNew = actionDisplayMap // Visualization after DP selection
        extService?.getDevice(deviceId)?.let {
            this.devIcon = it.iconUrl // Device icon
            this.isDevOnline = it.isOnline // Device online status
            this.entityName = it.name // Device name
        }
    }
    
  • DP of step for common devices

    val type = DeviceStepType.HIGH
    val builder = DeviceActionBuilder(deviceId, deviceActionDetailBean, selDpValue, selDpDisplayValue ?: "", isStep = true)
          .apply{
                this.setType(type)
        }
    val actionBase: ActionBase = builder.build() as ActionBase
    SceneAction(actionBase).apply {
        actionDisplayNew = actionDisplayMap // Visualization after DP selection
        extService?.getDevice(deviceId)?.let {
            this.devIcon = it.iconUrl // Device icon
            this.isDevOnline = it.isOnline // Device online status
            this.entityName = it.name // Device name
        }
    }
    
  • DP of temperature for common devices

    • The convertTemp map stores DP values in different temperature units.
    • originTempUnit specifies the temperature unit of the DP.
    • tempUnit specifies the app’s preferred temperature unit.
    • dpScale specifies the decimal places of the DP.
    val originTempUnit = "celsius" // The temperature unit of the DP.
    val tempUnit = "fahrenheit" // The app's preferred temperature unit.
    val dpScale = 1
    val convertTemp = mutableMapOf<String, Int>()
    convertTemp[originTempUnit] = 120
    convertTemp[tempUnit] = (120/10 * 1.8 + 32) * 10 // Convert Celsius to Fahrenheit
    
    val builder = DeviceActionBuilder(deviceId, deviceActionDetailBean, selDpValue, selDpDisplayValue ?: "")
        .apply {
            originTempUnit?.let {
                this.setOriginTempUnit(it)
            }
            dpScale?.let {
                this.setDpScale(it)
            }
            convertTemp?.let {
                this.setConvertTemp(it)
            }
        }
    val actionBase: ActionBase = builder.build() as ActionBase
        SceneAction(actionBase).apply {
        actionDisplayNew = actionDisplayMap // Visualization after DP selection
        extService?.getDevice(deviceId)?.let {
            this.devIcon = it.iconUrl // Device icon
            this.isDevOnline = it.isOnline // Device online status
            this.entityName = it.name // Device name
        }
    }
    
  • Group action

    val builder = DeviceActionBuilder(groupId.toString(), deviceActionDetailBean, selDpValue, selDpDisplayValue ?: "")
    val actionBase: ActionBase = builder.build() as ActionBase
    SceneAction(actionBase).apply {
        actionDisplayNew = actionDisplayMap // Visualization after DP selection
        this.actionExecutor = ACTION_TYPE_DEVICE_GROUP // The group actionExecutor
        extService?.getGroupDevice(groupId)?.let {
            this.devIcon = it.iconUrl // Group icon
            this.isDevOnline = it.isOnline // Group online status
            this.entityName = it.name // Group name
        }
    }