ควบคุมอุปกรณ์ภายนอก

ใน Android 11 ขึ้นไป ฟีเจอร์การควบคุมอุปกรณ์การเข้าถึงด่วนจะช่วยให้ผู้ใช้ดูและควบคุมอุปกรณ์ภายนอก เช่น หลอดไฟ ตัวควบคุมอุณหภูมิ และกล้องได้อย่างรวดเร็วจากสิ่งต่างๆ ที่ผู้ใช้สามารถทำได้ภายใน 3 การโต้ตอบจาก Launcher เริ่มต้น OEM ของอุปกรณ์จะเลือก Launcher ที่จะใช้ ผู้รวบรวมข้อมูลอุปกรณ์ เช่น Google Home และแอปของผู้ให้บริการบุคคลที่สามสามารถแสดงอุปกรณ์ในพื้นที่นี้ได้ หน้านี้จะแสดงวิธีแสดงการควบคุมอุปกรณ์ในพื้นที่นี้และลิงก์กับแอปควบคุม

รูปที่ 1 พื้นที่ควบคุมอุปกรณ์ใน UI ของ Android

หากต้องการเพิ่มการรองรับนี้ ให้สร้างและประกาศ ControlsProviderService สร้างการควบคุมที่แอปรองรับตามประเภทการควบคุมที่กำหนดไว้ล่วงหน้า จากนั้นสร้างผู้เผยแพร่สำหรับการควบคุมเหล่านี้

ส่วนติดต่อผู้ใช้

อุปกรณ์จะแสดงในส่วนการควบคุมอุปกรณ์เป็นวิดเจ็ตเทมเพลต วิดเจ็ตการควบคุมอุปกรณ์มี 5 รายการ ดังที่แสดงในภาพต่อไปนี้

สลับวิดเจ็ต
สลับ
สลับด้วยวิดเจ็ตแถบเลื่อน
สลับด้วยแถบเลื่อน
วิดเจ็ตช่วง
ช่วง (เปิดหรือปิดไม่ได้)
วิดเจ็ตเปิด/ปิดแบบไม่มีสถานะ
ปุ่มเปิด/ปิดแบบไม่เก็บสถานะ
วิดเจ็ตแผงอุณหภูมิ (ปิดอยู่)
แผงควบคุมอุณหภูมิ (ปิดอยู่)
รูปที่ 2 คอลเล็กชันวิดเจ็ตที่ใช้เทมเพลต

การแตะวิดเจ็ตค้างไว้จะนำคุณไปยังแอปเพื่อให้ควบคุมได้ละเอียดยิ่งขึ้น คุณสามารถปรับแต่งไอคอนและสีในวิดเจ็ตแต่ละรายการได้ แต่หากต้องการให้ผู้ใช้ได้รับประสบการณ์การใช้งานที่ดีที่สุด ให้ใช้ไอคอนและสีเริ่มต้นหากชุดค่าเริ่มต้นตรงกับอุปกรณ์

รูปภาพแสดงวิดเจ็ตแผงอุณหภูมิ (เปิดอยู่)
รูปที่ 3 เปิดวิดเจ็ตแผงอุณหภูมิ

สร้างบริการ

ส่วนนี้จะแสดงวิธีสร้าง ControlsProviderService บริการนี้จะบอก UI ระบบ Android ว่าแอปของคุณมีการควบคุมอุปกรณ์ซึ่งต้องแสดงในพื้นที่การควบคุมอุปกรณ์ของ UI Android

ControlsProviderService API ถือว่าผู้ใช้คุ้นเคยกับสตรีมแบบเรียลไทม์ตามที่ระบุไว้ในโปรเจ็กต์ Reactive Streams ใน GitHub และใช้งานในอินเทอร์เฟซ Flow ของ Java 9 API สร้างขึ้นจากแนวคิดต่อไปนี้

  • ผู้เผยแพร่: แอปพลิเคชันของคุณเป็นผู้เผยแพร่
  • ผู้สมัครใช้บริการ: UI ของระบบเป็นผู้สมัครใช้บริการและสามารถขอการควบคุมจำนวนหนึ่งจากผู้เผยแพร่โฆษณา
  • การสมัครใช้บริการ: กรอบเวลาที่ผู้เผยแพร่โฆษณาสามารถส่งการอัปเดตไปยัง UI ของระบบได้ ผู้เผยแพร่โฆษณาหรือผู้สมัครใช้บริการจะปิดหน้าต่างนี้ได้

ประกาศบริการ

แอปของคุณต้องประกาศบริการ เช่น MyCustomControlService ในไฟล์ Manifest ของแอป

บริการต้องมีตัวกรอง Intent สำหรับ ControlsProviderService ตัวกรองนี้ช่วยให้แอปพลิเคชันมีส่วนร่วมในการควบคุม UI ของระบบ

นอกจากนี้ คุณต้องมี label ที่แสดงในการควบคุมใน UI ของระบบด้วย

ตัวอย่างต่อไปนี้แสดงวิธีประกาศบริการ

<service
    android:name="MyCustomControlService"
    android:label="My Custom Controls"
    android:permission="android.permission.BIND_CONTROLS"
    android:exported="true"
    >
    <intent-filter>
      <action android:name="android.service.controls.ControlsProviderService" />
    </intent-filter>
</service>

ต่อไป ให้สร้างไฟล์ Kotlin ใหม่ชื่อ MyCustomControlService.kt และทำให้ไฟล์นั้น extends ControlsProviderService() ดังนี้

Kotlin

    class MyCustomControlService : ControlsProviderService() {
        ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
        ...
    }
    

เลือกประเภทการควบคุมที่ถูกต้อง

API มีเมธอดการสร้างเพื่อสร้างตัวควบคุม หากต้องการป้อนข้อมูลในเครื่องมือสร้าง ให้ระบุอุปกรณ์ที่ต้องการควบคุมและวิธีที่ผู้ใช้โต้ตอบกับอุปกรณ์ ทําตามขั้นตอนต่อไปนี้

  1. เลือกประเภทอุปกรณ์ที่การควบคุมแสดง คลาส DeviceTypes คือรายการอุปกรณ์ทั้งหมดที่รองรับ ระบบจะใช้ประเภทนี้เพื่อกำหนดไอคอนและสีของอุปกรณ์ใน UI
  2. กำหนดชื่อที่แสดงต่อผู้ใช้ ตำแหน่งอุปกรณ์ เช่น ห้องครัว และองค์ประกอบข้อความ UI อื่นๆ ที่เชื่อมโยงกับการควบคุม
  3. เลือกเทมเพลตที่ดีที่สุดเพื่อรองรับการโต้ตอบของผู้ใช้ การควบคุมจะได้รับการกำหนดค่า ControlTemplate จากแอปพลิเคชัน เทมเพลตนี้จะแสดงสถานะการควบคุมต่อผู้ใช้โดยตรง รวมถึงวิธีการป้อนข้อมูลที่ใช้ได้ นั่นคือ ControlAction ตารางต่อไปนี้แสดงเทมเพลตบางส่วนที่มีและการดำเนินการที่เทมเพลตรองรับ
เทมเพลต การดำเนินการ คำอธิบาย
ControlTemplate.getNoTemplateObject() None แอปพลิเคชันอาจใช้เพื่อสื่อข้อมูลเกี่ยวกับการควบคุม แต่ผู้ใช้ไม่สามารถโต้ตอบกับการควบคุมได้
ToggleTemplate BooleanAction แสดงการควบคุมที่สามารถสลับระหว่างสถานะเปิดใช้และปิดใช้ ออบเจ็กต์ BooleanAction มีช่องที่เปลี่ยนแปลงเพื่อแสดงสถานะใหม่ที่ขอเมื่อผู้ใช้แตะตัวควบคุม
RangeTemplate FloatAction แสดงวิดเจ็ตแถบเลื่อนที่มีค่าต่ำสุด สูงสุด และระยะห่างที่ระบุ เมื่อผู้ใช้โต้ตอบกับแถบเลื่อน ให้ส่งออบเจ็กต์ FloatAction ใหม่กลับไปยังแอปพลิเคชันพร้อมค่าที่อัปเดต
ToggleRangeTemplate BooleanAction, FloatAction เทมเพลตนี้เป็นการผสมผสานระหว่าง ToggleTemplate และ RangeTemplate โดยรองรับเหตุการณ์การแตะและแถบเลื่อน เช่น เพื่อควบคุมไฟหรี่
TemperatureControlTemplate ModeAction, BooleanAction, FloatAction นอกจากการรวมการดำเนินการก่อนหน้าแล้ว เทมเพลตนี้ยังช่วยให้ผู้ใช้ตั้งค่าโหมดต่างๆ เช่น ทำความร้อน ทำความเย็น ทำความร้อน/ทำความเย็น อีโค หรือปิด
StatelessTemplate CommandAction ใช้เพื่อระบุตัวควบคุมที่รองรับการสัมผัสแต่ไม่สามารถระบุสถานะได้ เช่น รีโมตทีวี IR คุณสามารถใช้เทมเพลตนี้เพื่อกำหนดกิจวัตรหรือมาโคร ซึ่งเป็นการรวมการควบคุมและการเปลี่ยนแปลงสถานะ

ข้อมูลนี้จะช่วยให้คุณสร้างการควบคุมได้ดังนี้

  • ใช้คลาสControl.StatelessBuilder บิลเดอร์เมื่อไม่ทราบสถานะของการควบคุม
  • ใช้คลาสบิลเดอร์ Control.StatefulBuilder เมื่อทราบสถานะของตัวควบคุม

เช่น หากต้องการควบคุมหลอดไฟอัจฉริยะและตัวควบคุมอุณหภูมิ ให้เพิ่มค่าคงที่ต่อไปนี้ลงใน MyCustomControlService

Kotlin

    private const val LIGHT_ID = 1234
    private const val LIGHT_TITLE = "My fancy light"
    private const val LIGHT_TYPE = DeviceTypes.TYPE_LIGHT
    private const val THERMOSTAT_ID = 5678
    private const val THERMOSTAT_TITLE = "My fancy thermostat"
    private const val THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT
 
    class MyCustomControlService : ControlsProviderService() {
      ...
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
    private final int LIGHT_ID = 1337;
    private final String LIGHT_TITLE = "My fancy light";
    private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
    private final int THERMOSTAT_ID = 1338;
    private final String THERMOSTAT_TITLE = "My fancy thermostat";
    private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
    ...
    }
    

สร้างผู้เผยแพร่โฆษณาสําหรับการควบคุม

หลังจากสร้างกลุ่มควบคุมแล้ว จะต้องมีผู้เผยแพร่โฆษณา ผู้เผยแพร่โฆษณาแจ้ง UI ของระบบว่ามีการควบคุมอยู่ คลาส ControlsProviderService มีเมธอดของผู้เผยแพร่โฆษณา 2 รายการที่คุณต้องลบล้างในโค้ดแอปพลิเคชัน

  • createPublisherForAllAvailable(): สร้าง Publisher สำหรับการควบคุมทั้งหมดที่มีในแอปของคุณ ใช้ Control.StatelessBuilder()เพื่อสร้างออบเจ็กต์ Control สำหรับผู้เผยแพร่โฆษณารายนี้
  • createPublisherFor(): สร้าง Publisher สำหรับรายการการควบคุมที่ระบุ ตามที่ระบุโดยตัวระบุสตริง ใช้ Control.StatefulBuilder เพื่อสร้างออบเจ็กต์ Control เหล่านี้ เนื่องจากผู้เผยแพร่โฆษณาต้องกำหนดสถานะให้กับการควบคุมแต่ละรายการ

สร้างผู้เผยแพร่

เมื่อแอปเผยแพร่ตัวควบคุมไปยัง UI ของระบบเป็นครั้งแรก แอปจะไม่ทราบว่าตัวควบคุมแต่ละรายการมีสถานะใด การดึงข้อมูลสถานะอาจใช้เวลานานเนื่องจากมีการส่งต่อข้อมูลหลายครั้งในเครือข่ายของผู้ให้บริการอุปกรณ์ ใช้วิธี createPublisherForAllAvailable() เพื่อแสดงการควบคุมที่ใช้ได้กับระบบ วิธีนี้ใช้Control.StatelessBuilderคลาสตัวสร้าง เนื่องจากสถานะของตัวควบคุมแต่ละรายการไม่เป็นที่รู้จัก

เมื่อตัวควบคุมปรากฏใน UI ของ Android ผู้ใช้จะเลือกตัวควบคุมที่ชอบได้

หากต้องการใช้ Coroutines ของ Kotlin เพื่อสร้าง ControlsProviderService ให้เพิ่มข้อกำหนดใหม่ลงใน build.gradle ดังนี้

Groovy

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4"
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk9:1.6.4")
}

เมื่อซิงค์ไฟล์ Gradle แล้ว ให้เพิ่มข้อมูลโค้ดต่อไปนี้ลงใน Service เพื่อนำไปใช้กับ createPublisherForAllAvailable()

Kotlin

    class MyCustomControlService : ControlsProviderService() {
 
      override fun createPublisherForAllAvailable(): Flow.Publisher =
          flowPublish {
              send(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE))
              send(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE))
          }
 
      private fun createStatelessControl(id: Int, title: String, type: Int): Control {
          val intent = Intent(this, MainActivity::class.java)
              .putExtra(EXTRA_MESSAGE, title)
              .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
          val action = PendingIntent.getActivity(
              this,
              id,
              intent,
              PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
          )
 
          return Control.StatelessBuilder(id.toString(), action)
              .setTitle(title)
              .setDeviceType(type)
              .build()
      }
 
          override fun createPublisherFor(controlIds: List): Flow.Publisher {
           TODO()
        }
 
        override fun performControlAction(
            controlId: String,
            action: ControlAction,
            consumer: Consumer
        ) {
            TODO()
        }
    }
    

Java

    public class MyCustomJavaControlService extends ControlsProviderService {
 
        private final int LIGHT_ID = 1337;
        private final String LIGHT_TITLE = "My fancy light";
        private final int LIGHT_TYPE = DeviceTypes.TYPE_LIGHT;
        private final int THERMOSTAT_ID = 1338;
        private final String THERMOSTAT_TITLE = "My fancy thermostat";
        private final int THERMOSTAT_TYPE = DeviceTypes.TYPE_THERMOSTAT;
 
        private boolean toggleState = false;
        private float rangeState = 18f;
        private final Map> controlFlows = new HashMap<>();
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherForAllAvailable() {
            List controls = new ArrayList<>();
            controls.add(createStatelessControl(LIGHT_ID, LIGHT_TITLE, LIGHT_TYPE));
            controls.add(createStatelessControl(THERMOSTAT_ID, THERMOSTAT_TITLE, THERMOSTAT_TYPE));
            return FlowAdapters.toFlowPublisher(Flowable.fromIterable(controls));
        }
 
        @NonNull
        @Override
        public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
            ReplayProcessor updatePublisher = ReplayProcessor.create();
 
            controlIds.forEach(control -> {
                controlFlows.put(control, updatePublisher);
                updatePublisher.onNext(createLight());
                updatePublisher.onNext(createThermostat());
            });
 
            return FlowAdapters.toFlowPublisher(updatePublisher);
        }
    }
    

ปัดเมนูระบบลงแล้วหาปุ่มการควบคุมอุปกรณ์ดังที่แสดงในรูปที่ 4

รูปภาพแสดง UI ของระบบสำหรับการควบคุมอุปกรณ์
รูปที่ 4 การควบคุมอุปกรณ์ในเมนูระบบ

การแตะการควบคุมอุปกรณ์จะนำคุณไปยังหน้าจอที่ 2 ซึ่งคุณจะเลือกแอปได้ เมื่อเลือกแอปแล้ว คุณจะเห็นวิธีที่ข้อมูลโค้ดก่อนหน้าสร้างเมนูระบบที่กำหนดเองซึ่งแสดงการควบคุมใหม่ดังที่แสดงในรูปที่ 5

รูปภาพแสดงเมนูระบบที่มีการควบคุมไฟและตัวควบคุมอุณหภูมิ
รูปที่ 5 การควบคุมไฟและตัวควบคุมอุณหภูมิที่จะเพิ่ม

ตอนนี้ให้ใช้เมธอด createPublisherFor() โดยเพิ่มข้อมูลต่อไปนี้ลงใน Service

Kotlin

    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    private val controlFlows = mutableMapOf>()
 
    private var toggleState = false
    private var rangeState = 18f
 
    override fun createPublisherFor(controlIds: List): Flow.Publisher {
        val flow = MutableSharedFlow(replay = 2, extraBufferCapacity = 2)
 
        controlIds.forEach { controlFlows[it] = flow }
 
        scope.launch {
            delay(1000) // Retrieving the toggle state.
            flow.tryEmit(createLight())
 
            delay(1000) // Retrieving the range state.
            flow.tryEmit(createThermostat())
 
        }
        return flow.asPublisher()
    }
 
    private fun createLight() = createStatefulControl(
        LIGHT_ID,
        LIGHT_TITLE,
        LIGHT_TYPE,
        toggleState,
        ToggleTemplate(
            LIGHT_ID.toString(),
            ControlButton(
                toggleState,
                toggleState.toString().uppercase(Locale.getDefault())
            )
        )
    )
 
    private fun createThermostat() = createStatefulControl(
        THERMOSTAT_ID,
        THERMOSTAT_TITLE,
        THERMOSTAT_TYPE,
        rangeState,
        RangeTemplate(
            THERMOSTAT_ID.toString(),
            15f,
            25f,
            rangeState,
            0.1f,
            "%1.1f"
        )
    )
 
    private fun  createStatefulControl(id: Int, title: String, type: Int, state: T, template: ControlTemplate): Control {
        val intent = Intent(this, MainActivity::class.java)
            .putExtra(EXTRA_MESSAGE, "$title $state")
            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        val action = PendingIntent.getActivity(
            this,
            id,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
 
        return Control.StatefulBuilder(id.toString(), action)
            .setTitle(title)
            .setDeviceType(type)
            .setStatus(Control.STATUS_OK)
            .setControlTemplate(template)
            .build()
    }
 
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
 
    

Java

    @NonNull
    @Override
    public Flow.Publisher createPublisherFor(@NonNull List controlIds) {
        ReplayProcessor updatePublisher = ReplayProcessor.create();
 
        controlIds.forEach(control -> {
            controlFlows.put(control, updatePublisher);
            updatePublisher.onNext(createLight());
            updatePublisher.onNext(createThermostat());
        });
 
        return FlowAdapters.toFlowPublisher(updatePublisher);
    }
 
    private Control createStatelessControl(int id, String title, int type) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, title)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatelessBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .build();
    }
 
    private Control createLight() {
        return createStatefulControl(
                LIGHT_ID,
                LIGHT_TITLE,
                LIGHT_TYPE,
                toggleState,
                new ToggleTemplate(
                        LIGHT_ID + "",
                        new ControlButton(
                                toggleState,
                                String.valueOf(toggleState).toUpperCase(Locale.getDefault())
                        )
                )
        );
    }
 
    private Control createThermostat() {
        return createStatefulControl(
                THERMOSTAT_ID,
                THERMOSTAT_TITLE,
                THERMOSTAT_TYPE,
                rangeState,
                new RangeTemplate(
                        THERMOSTAT_ID + "",
                        15f,
                        25f,
                        rangeState,
                        0.1f,
                        "%1.1f"
                )
        );
    }
 
    private  Control createStatefulControl(int id, String title, int type, T state, ControlTemplate template) {
        Intent intent = new Intent(this, MainActivity.class)
                .putExtra(EXTRA_MESSAGE, "$title $state")
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent action = PendingIntent.getActivity(
                this,
                id,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
 
        return new Control.StatefulBuilder(id + "", action)
                .setTitle(title)
                .setDeviceType(type)
                .setStatus(Control.STATUS_OK)
                .setControlTemplate(template)
                .build();
    }
    

ในตัวอย่างนี้ เมธอด createPublisherFor() มีการใช้งานจำลองสิ่งที่แอปต้องทำ ซึ่งก็คือ สื่อสารกับอุปกรณ์เพื่อดึงสถานะ และส่งสถานะนั้นไปยังระบบ

เมธอด createPublisherFor() ใช้ Coroutines และ Flow ของ Kotlin เพื่อตอบสนอง Reactive Streams API ที่จําเป็นโดยทําดังนี้

  1. สร้าง Flow
  2. รอ 1 วินาที
  3. สร้างและส่งสถานะหลอดไฟอัจฉริยะ
  4. รออีก 1 วินาที
  5. สร้างและส่งสถานะของตัวควบคุมอุณหภูมิ

จัดการการดำเนินการ

เมธอด performControlAction() จะส่งสัญญาณเมื่อผู้ใช้โต้ตอบกับตัวควบคุมที่เผยแพร่ ประเภทของ ControlAction ที่ส่งจะเป็นตัวกำหนดการดำเนินการ ดำเนินการที่เหมาะสมสำหรับการควบคุมที่ระบุ แล้วอัปเดตสถานะของอุปกรณ์ใน UI ของ Android

เพิ่มข้อมูลต่อไปนี้ลงใน Service เพื่อทําตามตัวอย่างให้เสร็จสมบูรณ์

Kotlin

    override fun performControlAction(
        controlId: String,
        action: ControlAction,
        consumer: Consumer
    ) {
        controlFlows[controlId]?.let { flow ->
            when (controlId) {
                LIGHT_ID.toString() -> {
                    consumer.accept(ControlAction.RESPONSE_OK)
                    if (action is BooleanAction) toggleState = action.newState
                    flow.tryEmit(createLight())
                }
                THERMOSTAT_ID.toString() -> {
                    consumer.accept(ControlAction.RESPONSE_OK)
                    if (action is FloatAction) rangeState = action.newValue
                    flow.tryEmit(createThermostat())
                }
                else -> consumer.accept(ControlAction.RESPONSE_FAIL)
            }
        } ?: consumer.accept(ControlAction.RESPONSE_FAIL)
    }
    

Java

    @Override
    public void performControlAction(@NonNull String controlId, @NonNull ControlAction action, @NonNull Consumer consumer) {
        ReplayProcessor processor = controlFlows.get(controlId);
        if (processor == null) return;
 
        if (controlId.equals(LIGHT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof BooleanAction) toggleState = ((BooleanAction) action).getNewState();
            processor.onNext(createLight());
        }
        if (controlId.equals(THERMOSTAT_ID + "")) {
            consumer.accept(ControlAction.RESPONSE_OK);
            if (action instanceof FloatAction) rangeState = ((FloatAction) action).getNewValue()
            processor.onNext(createThermostat());
        }
    }
    

เรียกใช้แอป เข้าถึงเมนูการควบคุมอุปกรณ์ และดูการควบคุมหลอดไฟและตัวควบคุมอุณหภูมิ

รูปภาพแสดงการควบคุมไฟและตัวควบคุมอุณหภูมิ
รูปที่ 6 การควบคุมไฟและตัวควบคุมอุณหภูมิ