Python untuk IoT

OOP Python: Object-Oriented Programming untuk IoT

TOKEN

Panduan lengkap memahami dan menerapkan OOP dalam Python untuk membangun sistem IoT yang terstruktur, modular, dan mudah dipelihara

1. Apa Itu OOP?

Object-Oriented Programming (OOP) adalah paradigma pemrograman yang mengorganisir kode ke dalam bentuk "objek" — entitas yang memiliki data (atribut/properti) dan perilaku (method/fungsi). Berbeda dari pemrograman prosedural yang berfokus pada urutan eksekusi fungsi, OOP berfokus pada modeling dunia nyata ke dalam kode.

Dalam konteks Internet of Things (IoT), OOP menjadi sangat powerful karena sistem IoT secara alami terdiri dari entitas-entitas yang bisa dimodelkan sebagai objek: sensor, aktuator, perangkat, gateway, pesan, dan lainnya. Setiap entitas ini memiliki data tersendiri dan perilaku tersendiri — persis seperti konsep objek dalam OOP.

Empat Pilar OOP

Pilar Penjelasan Singkat Contoh di IoT
InheritanceKelas anak mewarisi atribut & method dari kelas indukSensorSuhu mewarisi dari kelas Sensor
PolymorphismMethod yang sama bisa berperilaku berbeda di tiap kelasMetode baca() berbeda untuk sensor suhu vs sensor gas
EncapsulationMenyembunyikan detail internal, hanya ekspos interface publikMethod kalibrasi sensor disembunyikan, hanya get_data() yang publik
AbstractionMenyederhanakan kompleksitas dengan abstraksi tingkat tinggiKelas abstrak Device tanpa detail spesifik hardware

OOP vs Pemrograman Prosedural

Diagram: Perbandingan Paradigma
  PROSEDURAL                    OOP
  ──────────                    ───
  def baca_suhu():              class Sensor:
  def baca_kelembaban():            def baca(self):
  def kirim_mqtt():                 def kirim(self):
  def cek_threshold():              def cek_threshold(self):

  data = baca_suhu()            sensor = SensorSuhu(pin=4)
  if data > 30:                 data = sensor.baca()
      kirim_mqtt("panas")      if data > 30:
                                    sensor.kirim("panas")

  Fokus: urutan fungsi          Fokus: objek & interaksi
💡 Tips

Python secara teknis mendukung multi-paradigma — Anda bisa menulis kode prosedural, OOP, atau bahkan fungsional dalam satu file. Namun untuk proyek IoT yang berkembang, OOP akan menghemat banyak waktu karena kode lebih terstruktur dan mudah di-maintain.

2. Classes & Objects

Class adalah blueprint (cetakan) untuk membuat objek. Ibarat arsitektur rumah — satu desain bisa menghasilkan banyak rumah. Sedangkan object adalah instance (kejadian) nyata dari class tersebut — rumah yang sudah dibangun dari desain tadi.

2.1 — Membuat Class Pertama

Python — class_sensor.py
"""
Contoh Class Sensor Sederhana
BeebaneLabs - https://beebanelabs.pages.dev
"""

import random
import time


class Sensor:
    """Class dasar untuk semua sensor IoT."""

    # Class variable — dimiliki oleh SEMUA instance
    jumlah_sensor = 0

    def __init__(self, nama, pin, satuan=""):
        """
        Constructor — dipanggil otomatis saat object dibuat.

        Parameters:
            nama   : Nama identifikasi sensor
            pin    : Pin GPIO yang digunakan
            satuan : Satuan pengukuran (°C, %, ppm, dll)
        """
        # Instance variable — dimiliki oleh masing-masing object
        self.nama = nama
        self.pin = pin
        self.satuan = satuan
        self.data_terakhir = None
        self.aktif = False

        # Update class variable
        Sensor.jumlah_sensor += 1
        self.id = Sensor.jumlah_sensor

    def aktifkan(self):
        """Mengaktifkan sensor."""
        self.aktif = True
        print(f"[{self.nama}] Sensor diaktifkan pada pin {self.pin}")

    def matikan(self):
        """Menonaktifkan sensor."""
        self.aktif = False
        print(f"[{self.nama}] Sensor dimatikan")

    def baca(self):
        """Membaca data dari sensor (simulasi)."""
        if not self.aktif:
            print(f"[{self.nama}] ERROR: Sensor belum aktif!")
            return None
        # Simulasi pembacaan
        self.data_terakhir = round(random.uniform(20.0, 40.0), 2)
        return self.data_terakhir

    def status(self):
        """Mengembalikan status sensor."""
        return {
            "id": self.id,
            "nama": self.nama,
            "pin": self.pin,
            "aktif": self.aktif,
            "data_terakhir": self.data_terakhir
        }

    def __str__(self):
        """Representasi string dari object."""
        status_text = "AKTIF" if self.aktif else "NONAKTIF"
        return f"Sensor('{self.nama}', pin={self.pin}, status={status_text})"


# --- Membuat Object (Instance) dari Class ---
sensor_suhu = Sensor("Sensor Suhu DHT22", pin=4, satuan="°C")
sensor_gas = Sensor("Sensor Gas MQ-135", pin=17, satuan="ppm")

print(sensor_suhu)          # Sensor('Sensor Suhu DHT22', pin=4, status=NONAKTIF)
print(f"Total sensor: {Sensor.jumlah_sensor}")  # Total sensor: 2

# Mengaktifkan dan membaca
sensor_suhu.aktifkan()
data = sensor_suhu.baca()
print(f"Suhu: {data}{sensor_suhu.satuan}")  # Suhu: 28.45°C
print(sensor_suhu.status())
Output: Sensor('Sensor Suhu DHT22', pin=4, status=NONAKTIF) Total sensor: 2 [Sensor Suhu DHT22] Sensor diaktifkan pada pin 4 Suhu: 28.45°C {'id': 1, 'nama': 'Sensor Suhu DHT22', 'pin': 4, 'aktif': True, 'data_terakhir': 28.45}

2.2 — Memahami __init__ dan self

Konsep Penjelasan
__init__Constructor — method khusus yang otomatis dipanggil saat object dibuat. Digunakan untuk inisialisasi atribut awal.
selfReferensi ke object itu sendiri. Setiap method dalam class harus menerima self sebagai parameter pertama agar bisa mengakses atribut object.
__str__Method khusus yang dipanggil saat kita menggunakan print(object) atau str(object).
Class variableVariabel yang dimiliki oleh class itu sendiri (bukan instance). Diakses via NamaClass.var.
Instance variableVariabel yang dimiliki oleh masing-masing object. Diakses via self.var.

3. Inheritance (Pewarisan)

Inheritance memungkinkan kita membuat class baru berdasarkan class yang sudah ada. Class baru (anak/subclass) mewarisi semua atribut dan method dari class induk (parent/superclass), dan bisa menambahkan atau meng-override fitur sesuai kebutuhan.

Dalam proyek IoT, inheritance sangat berguna karena berbagai jenis sensor memiliki karakteristik dasar yang sama (nama, pin, status aktif) tetapi cara pembacaan datanya berbeda-beda.

Python — inheritance.py
"""
Inheritance pada Class Sensor IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""

import random
import time


class Sensor:
    """Class dasar (parent) untuk semua sensor."""

    def __init__(self, nama, pin, satuan=""):
        self.nama = nama
        self.pin = pin
        self.satuan = satuan
        self.data_terakhir = None
        self.aktif = False
        self.riwayat = []  # Menyimpan history pembacaan

    def aktifkan(self):
        self.aktif = True
        print(f"[{self.nama}] Aktif pada pin {self.pin}")

    def matikan(self):
        self.aktif = False
        print(f"[{self.nama}] Dimatikan")

    def baca(self):
        """Method dasar — akan di-override oleh subclass."""
        raise NotImplementedError("Subclass harus mengimplementasikan method baca()")

    def simpan_riwayat(self, nilai):
        """Menyimpan data ke riwayat pembacaan."""
        entry = {
            "waktu": time.strftime("%Y-%m-%d %H:%M:%S"),
            "nilai": nilai
        }
        self.riwayat.append(entry)
        # Batasi riwayat maksimal 100 entri
        if len(self.riwayat) > 100:
            self.riwayat.pop(0)

    def rata_rata(self):
        """Menghitung rata-rata dari riwayat pembacaan."""
        if not self.riwayat:
            return None
        nilai_list = [e["nilai"] for e in self.riwayat]
        return round(sum(nilai_list) / len(nilai_list), 2)


# ---- Child Class: Sensor Suhu ----
class SensorSuhu(Sensor):
    """Sensor suhu DHT11/DHT22."""

    def __init__(self, nama, pin, tipe_sensor="DHT22"):
        super().__init__(nama, pin, satuan="°C")
        self.tipe_sensor = tipe_sensor
        self.threshold_panas = 35.0

    def baca(self):
        """Override method baca() untuk sensor suhu."""
        if not self.aktif:
            return None
        # Simulasi: suhu antara 22-38°C
        suhu = round(random.uniform(22.0, 38.0), 1)
        self.data_terakhir = suhu
        self.simpan_riwayat(suhu)
        return suhu

    def cek_panas(self):
        """Method khusus sensor suhu."""
        if self.data_terakhir and self.data_terakhir > self.threshold_panas:
            return True
        return False


# ---- Child Class: Sensor Kelembaban ----
class SensorKelembaban(Sensor):
    """Sensor kelembaban tanah atau udara."""

    def __init__(self, nama, pin, tipe="tanah"):
        super().__init__(nama, pin, satuan="%")
        self.tipe = tipe  # "tanah" atau "udara"
        self.threshold_kering = 30.0

    def baca(self):
        """Override method baca() untuk sensor kelembaban."""
        if not self.aktif:
            return None
        kelembaban = round(random.uniform(20.0, 95.0), 1)
        self.data_terakhir = kelembaban
        self.simpan_riwayat(kelembaban)
        return kelembaban

    def cek_kering(self):
        """Method khusus sensor kelembaban."""
        if self.data_terakhir and self.data_terakhir < self.threshold_kering:
            return True
        return False


# ---- Child Class: Sensor Gas ----
class SensorGas(Sensor):
    """Sensor gas MQ-135 / MQ-2."""

    def __init__(self, nama, pin, tipe_gas="CO2"):
        super().__init__(nama, pin, satuan="ppm")
        self.tipe_gas = tipe_gas
        self.threshold_bahaya = 1000

    def baca(self):
        """Override method baca() untuk sensor gas."""
        if not self.aktif:
            return None
        ppm = round(random.uniform(200, 2000), 0)
        self.data_terakhir = ppm
        self.simpan_riwayat(ppm)
        return ppm

    def cek_bahaya(self):
        """Method khusus sensor gas."""
        if self.data_terakhir and self.data_terakhir > self.threshold_bahaya:
            return True
        return False


# --- Penggunaan ---
suhu = SensorSuhu("DHT22 Ruang Tamu", pin=4)
kelembaban = SensorKelembaban("Soil Moisture", pin=17, tipe="tanah")
gas = SensorGas("MQ-135 Dapur", pin=27, tipe_gas="LPG")

# Aktifkan semua
for sensor in [suhu, kelembaban, gas]:
    sensor.aktifkan()

# Baca data beberapa kali
for i in range(5):
    s = suhu.baca()
    k = kelembaban.baca()
    g = gas.baca()
    print(f"Pembacaan {i+1}: Suhu={s}°C, Kelembaban={k}%, Gas={g}ppm")
    time.sleep(0.5)

# Cek rata-rata
print(f"\nRata-rata suhu: {suhu.rata_rata()}°C")
print(f"Suhu panas? {suhu.cek_panas()}")
print(f"Tanah kering? {kelembaban.cek_kering()}")
print(f"Gas bahaya? {gas.cek_bahaya()}")

# isinstance() untuk mengecek tipe
print(f"\nApakah suhu termasuk Sensor? {isinstance(suhu, Sensor)}")  # True
print(f"Apakah gas termasuk SensorSuhu? {isinstance(gas, SensorSuhu)}")  # False

super() dan Method Resolution Order

Fungsi super() memanggil method dari class induk. Ini penting agar kita tidak perlu menulis ulang kode yang sudah ada di parent. Saat memanggil super().__init__(), kita meneruskan parameter ke constructor parent sehingga atribut dasar seperti nama, pin, dan satuan tetap terinisialisasi.

4. Polymorphism (Bentuk Jamak)

Polymorphism berarti "banyak bentuk" — memungkinkan method yang sama memiliki implementasi berbeda di setiap class. Dalam Python, polymorphism bekerja secara natural karena Python menggunakan duck typing: jika sebuah object memiliki method yang dibutuhkan, ia bisa digunakan tanpa peduli class-nya.

Python — polymorphism.py
"""
Polymorphism dalam Sistem IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""

import random
import json


class Aktuator:
    """Class dasar aktuator."""

    def __init__(self, nama, pin):
        self.nama = nama
        self.pin = pin
        self.status = False

    def nyalakan(self):
        self.status = True
        print(f"[{self.nama}] DINYALAKAN")

    def matikan(self):
        self.status = False
        print(f"[{self.nama}] DIMATIKAN")

    def eksekusi(self, perintah):
        """Method polymorphic — di-override oleh subclass."""
        raise NotImplementedError


class Lampu(Aktuator):
    """Aktuator lampu."""

    def __init__(self, nama, pin, warna="putih"):
        super().__init__(nama, pin)
        self.warna = warna
        self.brightness = 100

    def eksekusi(self, perintah):
        if perintah == "nyalakan":
            self.nyalakan()
        elif perintah == "matikan":
            self.matikan()
        elif perintah.startswith("brightness:"):
            level = int(perintah.split(":")[1])
            self.brightness = max(0, min(100, level))
            print(f"[{self.nama}] Brightness: {self.brightness}%")
        return self.status


class Relay(Aktuator):
    """Aktuator relay (untuk perangkat high-power)."""

    def __init__(self, nama, pin, tipe="NO"):
        super().__init__(nama, pin)
        self.tipe = tipe  # NO (Normally Open) atau NC (Normally Closed)

    def eksekusi(self, perintah):
        if perintah == "nyalakan":
            self.nyalakan()
        elif perintah == "matikan":
            self.matikan()
        elif perintah == "toggle":
            if self.status:
                self.matikan()
            else:
                self.nyalakan()
        return self.status


class Buzzer(Aktuator):
    """Aktuator buzzer/speaker."""

    def __init__(self, nama, pin):
        super().__init__(nama, pin)
        self.pattern = "continuous"

    def eksekusi(self, perintah):
        if perintah == "alarm":
            self.nyalakan()
            self.pattern = "alarm"
            print(f"[{self.nama}] 🔊 BUNYI ALARM!")
        elif perintah == "beep":
            print(f"[{self.nama}] Bip!")
        elif perintah == "matikan":
            self.matikan()
            self.pattern = "continuous"
        return self.status


# --- Polymorphism: satu fungsi, berbagai perilaku ---
def kontrol_semua(aktuator_list, perintah):
    """Mengirim perintah ke semua aktuator — polymorphism!"""
    for aktuator in aktuator_list:
        aktuator.eksekusi(perintah)


# Buat berbagai aktuator
lampu_ruang = Lampu("Lampu Ruang Tamu", pin=18, warna="kuning")
relay_pompa = Relay("Relay Pompa Air", pin=23)
buzzer_alarm = Buzzer("Buzzer Alarm", pin=24)

semua_aktuator = [lampu_ruang, relay_pompa, buzzer_alarm]

# Polymorphism: semua object punya .eksekusi() tapi berperilaku beda
print("=== Semua Aktuator ON ===")
kontrol_semua(semua_aktuator, "nyalakan")

print("\n=== Semua Aktuator OFF ===")
kontrol_semua(semua_aktuator, "matikan")

# Duck typing — tidak peduli tipe, asal punya method yang sama
print("\n=== Duck Typing ===")
for aktuator in semua_aktuator:
    # Semua punya .eksekusi(), jadi bisa dipanggil seragam
    aktuator.eksekusi("nyalakan")
    print(f"  → {aktuator.nama}: status={aktuator.status}")
    aktuator.eksekusi("matikan")
💡 Tips

Duck typing berarti Python tidak memeriksa tipe object, hanya memeriksa apakah object tersebut memiliki method yang dibutuhkan. "Jika berjalan seperti bebek dan berkwek seperti bebek, maka itu bebek." Ini membuat kode Python sangat fleksibel untuk sistem IoT yang punya banyak jenis perangkat.

5. Encapsulation (Pembungkusan)

Encapsulation adalah prinsip menyembunyikan detail internal object dan hanya mengekspos interface yang aman. Dalam Python, ini dilakukan dengan konvensi penamaan: single underscore _ untuk "protected" dan double underscore __ untuk "private" (name mangling).

Python — encapsulation.py
"""
Encapsulation pada Device IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""


class DeviceSecure:
    """
    Contoh encapsulation pada perangkat IoT.
    Atribut private dilindungi dari akses langsung.
    """

    def __init__(self, nama, ip_address, api_key):
        self.nama = nama                    # Public
        self._ip_address = ip_address       # Protected (konvensi)
        self.__api_key = api_key            # Private (name mangling)
        self.__koneksi_aktif = False
        self.__log = []

    # --- Property: getter ---
    @property
    def status_koneksi(self):
        """Mengecek status koneksi tanpa mengakses atribut private."""
        return "TERHUBUNG" if self.__koneksi_aktif else "TERPUTUS"

    @property
    def ip(self):
        """Getter untuk IP (read-only)."""
        return self._ip_address

    # --- Method publik (interface) ---
    def hubungkan(self):
        """Menghubungkan device — interface publik."""
        if self.__validasi_api_key():
            self.__koneksi_aktif = True
            self.__catat_log("Koneksi berhasil")
            print(f"[{self.nama}] Terhubung ke {self._ip_address}")
        else:
            self.__catat_log("Koneksi GAGAL: API key invalid")
            print(f"[{self.nama}] GAGAL: API key tidak valid")

    def putuskan(self):
        """Memutuskan koneksi."""
        self.__koneksi_aktif = False
        self.__catat_log("Koneksi diputus")
        print(f"[{self.nama}] Koneksi diputus")

    def kirim_data(self, data):
        """Mengirim data — hanya jika terhubung."""
        if not self.__koneksi_aktif:
            print(f"[{self.nama}] ERROR: Tidak terhubung!")
            return False
        self.__catat_log(f"Data dikirim: {data}")
        print(f"[{self.nama}] Data terkirim: {data}")
        return True

    def dapatkan_log(self):
        """Mengembalikan salinan log (bukan referensi asli)."""
        return self.__log.copy()

    # --- Method private ---
    def __validasi_api_key(self):
        """Validasi internal — tidak bisa diakses dari luar."""
        return len(self.__api_key) >= 10

    def __catat_log(self, pesan):
        """Logging internal."""
        import time
        entry = f"[{time.strftime('%H:%M:%S')}] {pesan}"
        self.__log.append(entry)


# --- Penggunaan ---
device = DeviceSecure("Gateway Utama", "192.168.1.100", "rahasia-api-key-12345")

# Akses publik — aman
print(device.nama)              # Gateway Utama
print(device.status_koneksi)    # TERPUTUS
print(device.ip)                # 192.168.1.100

device.hubungkan()
print(device.status_koneksi)    # TERHUBUNG
device.kirim_data({"suhu": 28.5})

# Akses private — akan error atau name mangling
try:
    print(device.__api_key)     # AttributeError!
except AttributeError as e:
    print(f"Error: {e}")        # 'DeviceSecure' object has no attribute '__api_key'

# Log hanya bisa diakses via method publik
print(f"Log entries: {len(device.dapatkan_log())}")
⚠️ Peringatan

Python tidak memiliki enkripsi "sejati" untuk atribut private. Double underscore __ hanya melakukan name mangling (atribut menjadi _NamaClass__atribut). Namun, ini sudah cukup untuk mencegah akses tidak sengaja dan mengirimkan pesan jelas bahwa atribut tersebut tidak boleh diakses langsung.

6. Abstraction (Abstraksi)

Abstraction adalah prinsip menyederhanakan kompleksitas dengan hanya menampilkan fitur yang relevan kepada pengguna. Dalam Python, abstraksi diimplementasikan menggunakan modul abc (Abstract Base Classes) yang memungkinkan kita mendefinisikan class abstrak — class yang tidak bisa diinstansiasi langsung dan harus di-override oleh subclass.

Python — abstraction.py
"""
Abstraction untuk Sistem IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""

from abc import ABC, abstractmethod
import json
import time
import random


class PerangkatIoT(ABC):
    """
    Abstract Base Class untuk semua perangkat IoT.
    Tidak bisa diinstansiasi langsung — HARUS di-override.
    """

    def __init__(self, nama, lokasi):
        self.nama = nama
        self.lokasi = lokasi
        self._terhubung = False

    @abstractmethod
    def inisialisasi(self):
        """Inisialisasi hardware — WAJIB diimplementasikan."""
        pass

    @abstractmethod
    def baca_data(self):
        """Membaca data dari perangkat — WAJIB diimplementasikan."""
        pass

    @abstractmethod
    def kirim_ke_server(self, data):
        """Mengirim data ke server — WAJIB diimplementasikan."""
        pass

    # Concrete method — sudah diimplementasikan, bisa dipakai langsung
    def format_json(self, data):
        """Format data menjadi JSON string."""
        payload = {
            "device": self.nama,
            "lokasi": self.lokasi,
            "waktu": time.strftime("%Y-%m-%d %H:%M:%S"),
            "data": data
        }
        return json.dumps(payload)

    def status(self):
        """Mengembalikan status perangkat."""
        return f"{self.nama} @ {self.lokasi} — {'ONLINE' if self._terhubung else 'OFFLINE'}"


class StasiunCuaca(PerangkatIoT):
    """Implementasi konkret: Stasiun pemantau cuaca."""

    def __init__(self, nama, lokasi):
        super().__init__(nama, lokasi)
        self.sensor_suhu = None
        self.sensor_kelembaban = None
        self.sensor_tekanan = None

    def inisialisasi(self):
        """Implementasi inisialisasi untuk stasiun cuaca."""
        self.sensor_suhu = True
        self.sensor_kelembaban = True
        self.sensor_tekanan = True
        self._terhubung = True
        print(f"[{self.nama}] Stasiun cuaca siap — 3 sensor aktif")

    def baca_data(self):
        """Implementasi pembacaan data stasiun cuaca."""
        if not self._terhubung:
            return None
        return {
            "suhu": round(random.uniform(22, 35), 1),
            "kelembaban": round(random.uniform(40, 90), 1),
            "tekanan": round(random.uniform(1000, 1025), 1)
        }

    def kirim_ke_server(self, data):
        """Implementasi pengiriman data ke server."""
        json_data = self.format_json(data)
        print(f"[{self.nama}] Mengirim: {json_data}")
        return True


class PemantauAir(PerangkatIoT):
    """Implementasi konkret: Pemantau kualitas air."""

    def __init__(self, nama, lokasi):
        super().__init__(nama, lokasi)
        self.sensor_ph = None
        self.sensor_turbidity = None

    def inisialisasi(self):
        self.sensor_ph = True
        self.sensor_turbidity = True
        self._terhubung = True
        print(f"[{self.nama}] Pemantau air siap — 2 sensor aktif")

    def baca_data(self):
        if not self._terhubung:
            return None
        return {
            "ph": round(random.uniform(6.5, 8.5), 2),
            "turbidity": round(random.uniform(0, 100), 1),
            "suhu_air": round(random.uniform(20, 30), 1)
        }

    def kirim_ke_server(self, data):
        json_data = self.format_json(data)
        print(f"[{self.nama}] Mengirim: {json_data}")
        return True


# --- Penggunaan ---
# PerangkatIoT("test", "sini")  # TypeError: Can't instantiate abstract class!

# Buat instance dari subclass
cuaca = StasiunCuaca("Stasiun A", "Atap Gedung")
air = PemantauAir("Pemantau Kolam", "Kolam Ikan")

# Inisialisasi
cuaca.inisialisasi()
air.inisialisasi()

# Baca dan kirim data
data_cuaca = cuaca.baca_data()
cuaca.kirim_ke_server(data_cuaca)

data_air = air.baca_data()
air.kirim_ke_server(data_air)

# Polymorphism: perlakukan semua perangkat sama
print("\n=== Status Semua Perangkat ===")
for perangkat in [cuaca, air]:
    print(perangkat.status())

7. OOP dalam Konteks IoT

Sekarang mari kita terapkan semua konsep OOP ke dalam skenario nyata IoT. Kita akan membangun sistem yang terdiri dari kelas Sensor dan Device yang saling berinteraksi.

7.1 — Class Sensor untuk Berbagai Tipe

Python — sensor_class.py
"""
Class Sensor untuk Sistem IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""

from abc import ABC, abstractmethod
import random
import time


class SensorBase(ABC):
    """Abstract base class untuk semua sensor."""

    def __init__(self, nama, pin, satuan="", interval_baca=5):
        self.nama = nama
        self.pin = pin
        self.satuan = satuan
        self.interval_baca = interval_baca
        self._aktif = False
        self._data_buffer = []
        self._max_buffer = 50

    @abstractmethod
    def _baca_hardware(self):
        """Akses hardware sesungguhnya — override di subclass."""
        pass

    def aktifkan(self):
        self._aktif = True
        print(f"[{self.nama}] ✓ Sensor aktif (pin {self.pin})")

    def baca(self):
        if not self._aktif:
            return None
        try:
            nilai = self._baca_hardware()
            self._buffer_data(nilai)
            return nilai
        except Exception as e:
            print(f"[{self.nama}] Error baca: {e}")
            return None

    def _buffer_data(self, nilai):
        entry = {"waktu": time.time(), "nilai": nilai}
        self._data_buffer.append(entry)
        if len(self._data_buffer) > self._max_buffer:
            self._data_buffer.pop(0)

    def statistik(self):
        if not self._data_buffer:
            return None
        nilai_list = [d["nilai"] for d in self._data_buffer]
        return {
            "min": min(nilai_list),
            "max": max(nilai_list),
            "avg": round(sum(nilai_list) / len(nilai_list), 2),
            "count": len(nilai_list)
        }


class SensorDHT(SensorBase):
    """Sensor suhu dan kelembaban DHT11/DHT22."""

    def __init__(self, nama, pin, tipe="DHT22"):
        super().__init__(nama, pin, satuan="°C/%", interval_baca=3)
        self.tipe = tipe
        self.suhu = None
        self.kelembaban = None

    def _baca_hardware(self):
        self.suhu = round(random.uniform(22, 38), 1)
        self.kelembaban = round(random.uniform(30, 90), 1)
        return {"suhu": self.suhu, "kelembaban": self.kelembaban}

    def baca(self):
        data = super().baca()
        return data


class SensorUltrasonik(SensorBase):
    """Sensor jarak ultrasonik HC-SR04."""

    def __init__(self, nama, pin_trigger, pin_echo):
        super().__init__(nama, pin_trigger, satuan="cm", interval_baca=1)
        self.pin_echo = pin_echo

    def _baca_hardware(self):
        jarak = round(random.uniform(2, 400), 1)
        return jarak


class SensorLDR(SensorBase):
    """Sensor cahaya LDR."""

    def __init__(self, nama, pin):
        super().__init__(nama, pin, satuan="lux", interval_baca=5)

    def _baca_hardware(self):
        cahaya = round(random.uniform(0, 1023), 0)
        return cahaya


# --- Contoh Penggunaan ---
dht = SensorDHT("DHT22 Ruang Server", pin=4)
jarak = SensorUltrasonik("HC-SR04 Tangki", pin_trigger=17, pin_echo=27)
cahaya = SensorLDR("LDR Taman", pin=22)

# Aktifkan semua
for s in [dht, jarak, cahaya]:
    s.aktifkan()

# Baca beberapa kali
for i in range(10):
    print(f"DHT: {dht.baca()}, Jarak: {jarak.baca()}cm, Cahaya: {cahaya.baca()}lux")
    time.sleep(0.2)

# Statistik
print(f"\nStatistik DHT: {dht.statistik()}")
print(f"Statistik Jarak: {jarak.statistik()}")

7.2 — Class Device sebagai Controller

Python — device_class.py
"""
Class Device sebagai IoT Controller
BeebaneLabs - https://beebanelabs.pages.dev
"""

import time
import json
from datetime import datetime


class IoTDevice:
    """
    Device controller yang mengelola sensor dan aktuator.
    Menjadi 'otak' dari node IoT.
    """

    def __init__(self, nama, device_id, lokasi=""):
        self.nama = nama
        self.device_id = device_id
        self.lokasi = lokasi
        self._sensor_list = {}
        self._aktuator_list = {}
        self._rules = []
        self._running = False

    def tambah_sensor(self, key, sensor):
        """Menambahkan sensor ke device."""
        self._sensor_list[key] = sensor
        print(f"[{self.nama}] Sensor '{key}' ditambahkan ({sensor.nama})")

    def tambah_aktuator(self, key, aktuator):
        """Menambahkan aktuator ke device."""
        self._aktuator_list[key] = aktuator
        print(f"[{self.nama}] Aktuator '{key}' ditambahkan ({aktuator.nama})")

    def tambah_rule(self, kondisi_fn, aksi_fn, deskripsi=""):
        """Menambahkan aturan otomatis."""
        self._rules.append({
            "kondisi": kondisi_fn,
            "aksi": aksi_fn,
            "deskripsi": deskripsi
        })
        print(f"[{self.nama}] Rule ditambahkan: {deskripsi}")

    def baca_semua_sensor(self):
        """Membaca semua sensor dan mengembalikan dict data."""
        hasil = {}
        for key, sensor in self._sensor_list.items():
            hasil[key] = sensor.baca()
        return hasil

    def jalankan_rules(self, data):
        """Evaluasi dan jalankan semua rules."""
        for rule in self._rules:
            try:
                if rule["kondisi"](data):
                    rule["aksi"](self._aktuator_list, data)
                    print(f"[{self.nama}] Rule dijalankan: {rule['deskripsi']}")
            except Exception as e:
                print(f"[{self.nama}] Rule error: {e}")

    def siklus(self):
        """Satu siklus baca-evaluasi-aksi."""
        print(f"\n--- Siklus {datetime.now().strftime('%H:%M:%S')} ---")
        data = self.baca_semua_sensor()
        print(f"Data sensor: {json.dumps(data, default=str)}")
        self.jalankan_rules(data)
        return data

    def jalankan(self, interval=5, max_siklus=None):
        """Menjalankan device dalam loop."""
        self._running = True
        siklus = 0
        print(f"\n[{self.nama}] === DEVICE STARTED ===")

        # Aktifkan semua sensor
        for sensor in self._sensor_list.values():
            sensor.aktifkan()

        try:
            while self._running:
                self.siklus()
                siklus += 1
                if max_siklus and siklus >= max_siklus:
                    break
                time.sleep(interval)
        except KeyboardInterrupt:
            print(f"\n[{self.nama}] Dihentikan oleh user")
        finally:
            self._running = False
            print(f"[{self.nama}] === DEVICE STOPPED ===")

    def stop(self):
        """Menghentikan device."""
        self._running = False


# --- Membuat Sistem IoT Lengkap ---
# (Asumsi class SensorDHT, Lampu, Relay dari contoh sebelumnya tersedia)

# device = IoTDevice("Smart Room", "SR-001", "Ruang Server")
# device.tambah_sensor("suhu", SensorDHT("DHT22", pin=4))
# device.tambah_aktuator("lampu", Lampu("LED Indikator", pin=18))
# device.tambah_aktuator("kipas", Relay("Kipas Exhaust", pin=23))
#
# # Tambah aturan otomatis
# device.tambah_rule(
#     kondisi_fn=lambda data: data.get("suhu", {}).get("suhu", 0) > 30,
#     aksi_fn=lambda akt, data: akt["kipas"].eksekusi("nyalakan"),
#     deskripsi="Nyalakan kipas jika suhu > 30°C"
# )
# device.tambah_rule(
#     kondisi_fn=lambda data: data.get("suhu", {}).get("suhu", 0) <= 30,
#     aksi_fn=lambda akt, data: akt["kipas"].eksekusi("matikan"),
#     deskripsi="Matikan kipas jika suhu <= 30°C"
# )
#
# # Jalankan 5 siklus
# device.jalankan(interval=2, max_siklus=5)
Diagram: Arsitektur OOP untuk IoT Device
┌─────────────────────────────────────────────────┐
│                 IoTDevice                        │
│  ┌─────────────┐     ┌──────────────────────┐   │
│  │ Sensors      │     │ Rules Engine         │   │
│  │ ┌─────────┐ │     │ ┌──────────────────┐ │   │
│  │ │ DHT22   │ │────►│ │ if suhu > 30:    │ │   │
│  │ │ HC-SR04 │ │     │ │   nyalakan(kipas)│ │   │
│  │ │ LDR     │ │     │ └──────────────────┘ │   │
│  │ └─────────┘ │     └──────────────────────┘   │
│  └─────────────┘              │                  │
│                              ▼                   │
│  ┌─────────────┐     ┌──────────────────────┐   │
│  │ Data Store  │     │ Aktuators            │   │
│  │ ┌─────────┐ │     │ ┌──────────────────┐ │   │
│  │ │ Buffer  │ │     │ │ Lampu, Relay,    │ │   │
│  │ │ Stats   │ │     │ │ Buzzer, Motor    │ │   │
│  │ └─────────┘ │     │ └──────────────────┘ │   │
│  └─────────────┘     └──────────────────────┘   │
└─────────────────────────────────────────────────┘

8. Design Patterns untuk IoT

Design patterns adalah solusi template untuk masalah desain yang umum. Dalam pengembangan sistem IoT, beberapa pattern sangat membantu untuk membuat kode yang bersih dan scalable.

8.1 — Singleton Pattern

Memastikan hanya ada satu instance dari sebuah class — cocok untuk MQTT client, database connection, atau konfigurasi global.

Python — singleton.py
"""
Singleton Pattern untuk MQTT Manager
BeebaneLabs - https://beebanelabs.pages.dev
"""


class MQTTManager:
    """Singleton — hanya satu instance yang boleh ada."""

    _instance = None
    _initialized = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, broker="localhost", port=1883):
        if MQTTManager._initialized:
            return
        self.broker = broker
        self.port = port
        self._connected = False
        self._subscribers = {}
        MQTTManager._initialized = True
        print(f"[MQTT] Manager dibuat — broker: {broker}:{port}")

    def connect(self):
        self._connected = True
        print(f"[MQTT] Terhubung ke {self.broker}:{self.port}")

    def subscribe(self, topic, callback):
        self._subscribers[topic] = callback
        print(f"[MQTT] Subscribe: {topic}")


# Semua variabel ini menunjuk ke INSTANCE YANG SAMA
mqtt1 = MQTTManager("192.168.1.10", 1883)
mqtt2 = MQTTManager("broker-lain.com", 8883)  # Parameter diabaikan!

print(f"mqtt1 is mqtt2: {mqtt1 is mqtt2}")  # True
print(f"Broker: {mqtt2.broker}")  # 192.168.1.10 (tetap dari inisialisasi pertama)

8.2 — Observer Pattern

Membangun sistem event-driven di mana perubahan pada satu komponen (sensor) secara otomatis memberitahu komponen lain (aktuator, logger, notifikasi).

Python — observer.py
"""
Observer Pattern untuk Event-Driven IoT
BeebaneLabs - https://beebanelabs.pages.dev
"""


class EventEmitter:
    """Base class untuk event system."""

    def __init__(self):
        self._listeners = {}

    def on(self, event_name, callback):
        """Mendaftarkan listener untuk event tertentu."""
        if event_name not in self._listeners:
            self._listeners[event_name] = []
        self._listeners[event_name].append(callback)

    def emit(self, event_name, data=None):
        """Memancarkan event ke semua listener."""
        if event_name in self._listeners:
            for callback in self._listeners[event_name]:
                callback(data)


class SensorMonitor(EventEmitter):
    """Sensor yang memancarkan event saat threshold terlampaui."""

    def __init__(self, nama, threshold_high=None, threshold_low=None):
        super().__init__()
        self.nama = nama
        self.threshold_high = threshold_high
        self.threshold_low = threshold_low
        self._nilai_terakhir = None

    def update(self, nilai):
        self._nilai_terakhir = nilai

        if self.threshold_high and nilai > self.threshold_high:
            self.emit("threshold_high", {
                "sensor": self.nama,
                "nilai": nilai,
                "threshold": self.threshold_high
            })

        if self.threshold_low and nilai < self.threshold_low:
            self.emit("threshold_low", {
                "sensor": self.nama,
                "nilai": nilai,
                "threshold": self.threshold_low
            })

        self.emit("data_update", {"sensor": self.nama, "nilai": nilai})


# --- Penggunaan ---
monitor_suhu = SensorMonitor("Suhu", threshold_high=35, threshold_low=15)
monitor_gas = SensorMonitor("Gas MQ-135", threshold_high=1000)

# Daftarkan listener
def alert_handler(data):
    print(f"🚨 ALERT: {data['sensor']} = {data['nilai']} (threshold: {data['threshold']})")

def log_handler(data):
    print(f"📝 LOG: {data['sensor']} → {data['nilai']}")

def dashboard_handler(data):
    print(f"📊 DASHBOARD: Update {data['sensor']}: {data['nilai']}")

monitor_suhu.on("threshold_high", alert_handler)
monitor_suhu.on("threshold_low", alert_handler)
monitor_suhu.on("data_update", log_handler)
monitor_suhu.on("data_update", dashboard_handler)
monitor_gas.on("threshold_high", alert_handler)

# Simulasi update
monitor_suhu.update(28)     # Normal
monitor_suhu.update(36.5)   # Threshold high triggered!
monitor_suhu.update(12)     # Threshold low triggered!
monitor_gas.update(1500)    # Gas bahaya!
Output: 📝 LOG: Suhu → 28 📊 DASHBOARD: Update Suhu: 28 🚨 ALERT: Suhu = 36.5 (threshold: 35) 📝 LOG: Suhu → 36.5 📊 DASHBOARD: Update Suhu: 36.5 🚨 ALERT: Suhu = 12 (threshold: 15) 📝 LOG: Suhu → 12 📊 DASHBOARD: Update Suhu: 12 🚨 ALERT: Gas MQ-135 = 1500 (threshold: 1000)

8.3 — Factory Pattern

Membuat object tanpa perlu tahu class spesifiknya — cocok untuk membuat sensor berdasarkan konfigurasi file.

Python — factory.py
"""
Factory Pattern untuk Membuat Sensor dari Config
BeebaneLabs - https://beebanelabs.pages.dev
"""

import json


class SensorFactory:
    """Factory untuk membuat object sensor dari konfigurasi."""

    _registry = {}

    @classmethod
    def register(cls, tipe, class_ref):
        """Mendaftarkan tipe sensor ke factory."""
        cls._registry[tipe] = class_ref

    @classmethod
    def buat(cls, config):
        """Membuat sensor dari dict konfigurasi."""
        tipe = config.get("tipe")
        if tipe not in cls._registry:
            raise ValueError(f"Tipe sensor tidak dikenal: {tipe}")

        sensor_class = cls._registry[tipe]
        return sensor_class(**{k: v for k, v in config.items() if k != "tipe"})

    @classmethod
    def buat_dari_file(cls, filepath):
        """Membuat semua sensor dari file JSON config."""
        with open(filepath, "r") as f:
            configs = json.load(f)

        sensors = []
        for cfg in configs:
            sensor = cls.buat(cfg)
            sensors.append(sensor)
        return sensors


# Register semua tipe sensor
# SensorFactory.register("dht22", SensorDHT)
# SensorFactory.register("ultrasonik", SensorUltrasonik)
# SensorFactory.register("ldr", SensorLDR)

# Contoh config JSON:
# [
#   {"tipe": "dht22", "nama": "Suhu Ruang", "pin": 4},
#   {"tipe": "ultrasonik", "nama": "Jarak Tangki", "pin_trigger": 17, "pin_echo": 27},
#   {"tipe": "ldr", "nama": "Cahaya Taman", "pin": 22}
# ]
📋 Ringkasan Design Patterns
Pattern Kegunaan di IoT
SingletonMQTT client, database connection, config manager — hanya satu instance
ObserverEvent-driven notifications — sensor threshold, alert system
FactoryMembuat sensor/aktuator dari konfigurasi — fleksibel dan scalable
StrategyGanti algoritma (filter data, format payload) secara dinamis
StateMenangani state perangkat (idle, running, error, sleep)

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang OOP dalam Python untuk IoT:

Pertanyaan 1: Apa yang dimaksud dengan "self" dalam method class Python?

a) Nama class itu sendiri
b) Referensi ke instance/object yang sedang memanggil method
c) Variabel global yang bisa diakses semua class
d) Pointer ke class induk (parent)

Pertanyaan 2: Apa fungsi dari super().__init__() dalam inheritance?

a) Membuat instance baru dari class anak
b) Memanggil constructor dari class induk
c) Menghapus class induk dari memori
d) Membuat class abstrak baru

Pertanyaan 3: Dalam encapsulation, apa arti double underscore (__) sebelum nama atribut?

a) Atribut menjadi public
b) Atribut menjadi class variable
c) Atribut menjadi private (name mangling)
d) Atribut menjadi deprecated

Pertanyaan 4: Design pattern apa yang memastikan hanya ada SATU instance dari sebuah class?

a) Observer Pattern
b) Factory Pattern
c) Singleton Pattern
d) Strategy Pattern

Pertanyaan 5: Apa yang terjadi jika kita mencoba membuat instance dari class abstrak yang masih memiliki method @abstractmethod?

a) Object berhasil dibuat dengan method kosong
b) Python akan mengabaikan @abstractmethod
c) TypeError: Can't instantiate abstract class
d) Object terbuat tapi semua method mengembalikan None