From 4ea239d4fd7f14c38efba8b458052d74b28d7bec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20De=20Keersmaeker?=
 <francois.dekeersmaeker@uclouvain.be>
Date: Wed, 11 Dec 2024 12:41:46 +0100
Subject: [PATCH] Added all devices

---
 .gitignore                                    |  7 ++
 pyproject.toml                                |  4 +-
 .../DeviceControl/CameraControl.py            | 46 ++++++++++
 .../DeviceControl/DeviceControl.py            | 62 ++++++++++++++
 .../DeviceControl/LightControl.py             | 67 +++++++++++++++
 .../DeviceControl/PlugControl.py              | 14 ++++
 .../DeviceControl/SmartThingsControl.py       | 34 ++++++++
 .../DeviceControl/SmartThingsLightControl.py  | 20 +++++
 .../DeviceControl/SmartThingsPlugControl.py   | 12 +++
 .../DeviceControl/TapoControl.py              | 26 ++++++
 .../DeviceControl/TapoPlugControl.py          | 12 +++
 .../DeviceControl/TuyaControl.py              | 27 ++++++
 smart_home_testbed/DeviceControl/__init__.py  | 10 +++
 .../DeviceState/CameraScreenshotState.py      | 51 +++++++++++
 smart_home_testbed/DeviceState/CameraState.py | 30 +++++++
 smart_home_testbed/DeviceState/DeviceState.py | 51 +++++++++++
 smart_home_testbed/DeviceState/HueState.py    | 77 +++++++++++++++++
 smart_home_testbed/DeviceState/LightState.py  | 27 ++++++
 .../DeviceState/ScreenshotState.py            | 46 ++++++++++
 .../DeviceState/SmartThingsState.py           | 72 ++++++++++++++++
 .../DeviceState/TapoLightState.py             | 26 ++++++
 .../DeviceState/TapoPlugState.py              | 19 +++++
 smart_home_testbed/DeviceState/TapoState.py   | 50 +++++++++++
 .../DeviceState/TpLinkPlugState.py            | 22 +++++
 smart_home_testbed/DeviceState/__init__.py    | 17 ++++
 smart_home_testbed/__init__.py                | 28 +++++++
 smart_home_testbed/devices/.gitignore         |  8 ++
 smart_home_testbed/devices/__init__.py        | 27 ++++++
 .../cameras/DLinkCamera/DLinkCamera.py        | 35 ++++++++
 .../devices/cameras/DLinkCamera/__init__.py   |  1 +
 .../devices/cameras/TapoCamera/TapoCamera.py  | 20 +++++
 .../devices/cameras/TapoCamera/__init__.py    |  1 +
 .../TapoCameraSmartThings.py                  | 59 +++++++++++++
 .../cameras/TapoCameraSmartThings/__init__.py |  1 +
 .../cameras/XiaomiCamera/XiaomiCamera.py      | 82 ++++++++++++++++++
 .../devices/cameras/XiaomiCamera/__init__.py  |  1 +
 .../devices/cameras/__init__.py               |  4 +
 smart_home_testbed/devices/init_device.py     | 20 +++++
 .../devices/lights/HueLight/HueLight.py       | 46 ++++++++++
 .../devices/lights/HueLight/__init__.py       |  1 +
 .../HueLightEssentials/HueLightEssentials.py  | 74 ++++++++++++++++
 .../lights/HueLightEssentials/__init__.py     |  1 +
 .../HueLightSmartThings.py                    | 14 ++++
 .../lights/HueLightSmartThings/__init__.py    |  1 +
 .../devices/lights/TapoLight/TapoLight.py     | 34 ++++++++
 .../devices/lights/TapoLight/__init__.py      |  1 +
 .../TapoLightSmartThings.py                   | 14 ++++
 .../lights/TapoLightSmartThings/__init__.py   |  1 +
 .../devices/lights/TuyaLight/TuyaLight.py     | 84 +++++++++++++++++++
 .../devices/lights/TuyaLight/__init__.py      |  1 +
 smart_home_testbed/devices/lights/__init__.py |  6 ++
 .../SmartThingsOutlet/SmartThingsOutlet.py    | 25 ++++++
 .../plugs/SmartThingsOutlet/__init__.py       |  1 +
 .../devices/plugs/TapoPlug/TapoPlug.py        | 12 +++
 .../devices/plugs/TapoPlug/__init__.py        |  1 +
 .../TapoPlugSmartThings.py                    | 14 ++++
 .../plugs/TapoPlugSmartThings/__init__.py     |  1 +
 .../devices/plugs/TpLinkPlug/TpLinkPlug.py    | 15 ++++
 .../devices/plugs/TpLinkPlug/__init__.py      |  1 +
 .../TpLinkPlugSmartThings.py                  | 14 ++++
 .../plugs/TpLinkPlugSmartThings/__init__.py   |  1 +
 .../plugs/TpLinkPlugTapo/TpLinkPlugTapo.py    | 13 +++
 .../devices/plugs/TpLinkPlugTapo/__init__.py  |  1 +
 .../devices/plugs/TuyaPlug/TuyaPlug.py        | 16 ++++
 .../devices/plugs/TuyaPlug/__init__.py        |  1 +
 smart_home_testbed/devices/plugs/__init__.py  |  7 ++
 66 files changed, 1515 insertions(+), 2 deletions(-)
 create mode 100644 smart_home_testbed/DeviceControl/CameraControl.py
 create mode 100644 smart_home_testbed/DeviceControl/DeviceControl.py
 create mode 100644 smart_home_testbed/DeviceControl/LightControl.py
 create mode 100644 smart_home_testbed/DeviceControl/PlugControl.py
 create mode 100644 smart_home_testbed/DeviceControl/SmartThingsControl.py
 create mode 100644 smart_home_testbed/DeviceControl/SmartThingsLightControl.py
 create mode 100644 smart_home_testbed/DeviceControl/SmartThingsPlugControl.py
 create mode 100644 smart_home_testbed/DeviceControl/TapoControl.py
 create mode 100644 smart_home_testbed/DeviceControl/TapoPlugControl.py
 create mode 100644 smart_home_testbed/DeviceControl/TuyaControl.py
 create mode 100644 smart_home_testbed/DeviceControl/__init__.py
 create mode 100644 smart_home_testbed/DeviceState/CameraScreenshotState.py
 create mode 100644 smart_home_testbed/DeviceState/CameraState.py
 create mode 100644 smart_home_testbed/DeviceState/DeviceState.py
 create mode 100644 smart_home_testbed/DeviceState/HueState.py
 create mode 100644 smart_home_testbed/DeviceState/LightState.py
 create mode 100644 smart_home_testbed/DeviceState/ScreenshotState.py
 create mode 100644 smart_home_testbed/DeviceState/SmartThingsState.py
 create mode 100644 smart_home_testbed/DeviceState/TapoLightState.py
 create mode 100644 smart_home_testbed/DeviceState/TapoPlugState.py
 create mode 100644 smart_home_testbed/DeviceState/TapoState.py
 create mode 100644 smart_home_testbed/DeviceState/TpLinkPlugState.py
 create mode 100644 smart_home_testbed/DeviceState/__init__.py
 create mode 100644 smart_home_testbed/__init__.py
 create mode 100644 smart_home_testbed/devices/.gitignore
 create mode 100644 smart_home_testbed/devices/__init__.py
 create mode 100644 smart_home_testbed/devices/cameras/DLinkCamera/DLinkCamera.py
 create mode 100644 smart_home_testbed/devices/cameras/DLinkCamera/__init__.py
 create mode 100644 smart_home_testbed/devices/cameras/TapoCamera/TapoCamera.py
 create mode 100644 smart_home_testbed/devices/cameras/TapoCamera/__init__.py
 create mode 100644 smart_home_testbed/devices/cameras/TapoCameraSmartThings/TapoCameraSmartThings.py
 create mode 100644 smart_home_testbed/devices/cameras/TapoCameraSmartThings/__init__.py
 create mode 100644 smart_home_testbed/devices/cameras/XiaomiCamera/XiaomiCamera.py
 create mode 100644 smart_home_testbed/devices/cameras/XiaomiCamera/__init__.py
 create mode 100644 smart_home_testbed/devices/cameras/__init__.py
 create mode 100644 smart_home_testbed/devices/init_device.py
 create mode 100644 smart_home_testbed/devices/lights/HueLight/HueLight.py
 create mode 100644 smart_home_testbed/devices/lights/HueLight/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/HueLightEssentials/HueLightEssentials.py
 create mode 100644 smart_home_testbed/devices/lights/HueLightEssentials/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/HueLightSmartThings/HueLightSmartThings.py
 create mode 100644 smart_home_testbed/devices/lights/HueLightSmartThings/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/TapoLight/TapoLight.py
 create mode 100644 smart_home_testbed/devices/lights/TapoLight/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/TapoLightSmartThings/TapoLightSmartThings.py
 create mode 100644 smart_home_testbed/devices/lights/TapoLightSmartThings/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/TuyaLight/TuyaLight.py
 create mode 100644 smart_home_testbed/devices/lights/TuyaLight/__init__.py
 create mode 100644 smart_home_testbed/devices/lights/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/SmartThingsOutlet/SmartThingsOutlet.py
 create mode 100644 smart_home_testbed/devices/plugs/SmartThingsOutlet/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TapoPlug/TapoPlug.py
 create mode 100644 smart_home_testbed/devices/plugs/TapoPlug/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TapoPlugSmartThings/TapoPlugSmartThings.py
 create mode 100644 smart_home_testbed/devices/plugs/TapoPlugSmartThings/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlug/TpLinkPlug.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlug/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/TpLinkPlugSmartThings.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlugTapo/TpLinkPlugTapo.py
 create mode 100644 smart_home_testbed/devices/plugs/TpLinkPlugTapo/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/TuyaPlug/TuyaPlug.py
 create mode 100644 smart_home_testbed/devices/plugs/TuyaPlug/__init__.py
 create mode 100644 smart_home_testbed/devices/plugs/__init__.py

diff --git a/.gitignore b/.gitignore
index 2853266..b996f63 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,9 @@
 # Config folders
 .venv
+.vscode
+
+# Cache folders
+__pycache__
+
+# Local test files
+test.py
diff --git a/pyproject.toml b/pyproject.toml
index bda1d18..dc882d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,5 +59,5 @@ dependencies = [
 ]
 
 [project.urls]
-"Homepage" = "https://forge.uclouvain.be/smart-home-network-security/signature-extraction"
-"Source" = "https://forge.uclouvain.be/smart-home-network-security/signature-extraction"
+"Homepage" = "https://forge.uclouvain.be/smart-home-network-security/smart-home-testbed"
+"Source" = "https://forge.uclouvain.be/smart-home-network-security/smart-home-testbed"
diff --git a/smart_home_testbed/DeviceControl/CameraControl.py b/smart_home_testbed/DeviceControl/CameraControl.py
new file mode 100644
index 0000000..284bc68
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/CameraControl.py
@@ -0,0 +1,46 @@
+import time
+from .DeviceControl import DeviceControl
+
+
+class CameraControl(DeviceControl):
+    """
+    Class to control a camera device.
+    """
+
+    # Stream event duration
+    stream_duration = 7
+
+
+    def stream(self) -> None:
+        """
+        Perform the `stream` event.
+        """
+        self.start_stream()
+        time.sleep(self.stream_duration)
+        self.stop_stream()
+
+
+    def start_stream(self) -> None:
+        """
+        Start the camera's video stream.
+        """
+        self.get_phone().shell(f"input tap {self.x_start} {self.y_start}")
+
+    
+    def do_stop_stream(self) -> None:
+        """
+        Stop the camera's video stream.
+        """
+        self.get_phone().shell(f"input tap {self.x_stop} {self.y_stop}")
+
+    
+    def stop_stream(self) -> None:
+        """
+        Check if the stream was successful,
+        then stop the stream.
+        """
+        # Check if stream was successful
+        self._was_last_stream_successful = self.get_state()
+        
+        if self._was_last_stream_successful:
+            self.do_stop_stream()
diff --git a/smart_home_testbed/DeviceControl/DeviceControl.py b/smart_home_testbed/DeviceControl/DeviceControl.py
new file mode 100644
index 0000000..a6cb0db
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/DeviceControl.py
@@ -0,0 +1,62 @@
+from ppadb.device import Device as AdbDevice
+from ppadb.client import Client as AdbClient
+import cv2
+import numpy as np
+
+
+class DeviceControl:
+    """
+    Abstract class which provides methods to control a device.
+    """
+
+    ADB_SERVER_IP   = "127.0.0.1"
+    ADB_SERVER_PORT = 5037
+
+
+    def get_phone(self) -> AdbDevice:
+        """
+        Get the adb device object.
+
+        Returns:
+            ppadb.device.Device: The adb device object.
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        adb = AdbClient(host=DeviceControl.ADB_SERVER_IP, port=DeviceControl.ADB_SERVER_PORT)
+        return adb.devices()[0]
+    
+
+    def start_app(self) -> None:
+        """
+        Start the device's app on the phone.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        phone.shell(f"monkey -p {self.android_package} -c android.intent.category.LAUNCHER 1")
+
+
+    def close_app(self) -> None:
+        """
+        Close the device's app on the device.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        phone.shell(f"am force-stop {self.android_package}")
+
+    
+    def screenshot(self) -> np.ndarray:
+        """
+        Take a screenshot of the device's screen.
+        
+        Returns:
+            numpy.ndarray: The screenshot as a numpy array.
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        screenshot_bytes = self.get_phone().screencap()
+        screenshot_array = np.frombuffer(screenshot_bytes, dtype=np.uint8)
+        return cv2.imdecode(screenshot_array, cv2.IMREAD_UNCHANGED)
diff --git a/smart_home_testbed/DeviceControl/LightControl.py b/smart_home_testbed/DeviceControl/LightControl.py
new file mode 100644
index 0000000..4ed4fa7
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/LightControl.py
@@ -0,0 +1,67 @@
+import random
+from .DeviceControl import DeviceControl
+
+
+class LightControl(DeviceControl):
+    """
+    Class to control a light bulb device.
+    """
+
+
+    def toggle(self) -> None:
+        """
+        Perform the toggle event on the light.
+        """
+        self.get_phone().shell(f"input tap {self.x} {self.y}")
+
+
+    def _set_light_attr(self, y: int) -> None:
+        """
+        Randomly set an attribute of the light
+        (brightness or color).
+        
+        Args:
+            y (int): The y-coordinate of the corresponding gauge.
+        """
+        x = random.randint(self.x_left_gauge, self.x_right_gauge)
+        self.get_phone().shell(f"input tap {x} {y}")
+
+    
+    def do_set_brightness(self) -> None:
+        """
+        Randomly set the brightness of the light.
+        """
+        self._set_light_attr(self.y_brightness)
+
+    
+    def set_brightness(self) -> None:
+        """
+        Template method to randomly set the brightness of the light.
+        The child class must implement the concrete method `is_on`,
+        which checks if the light is on.
+        """
+        # If light is off, turn it on
+        if self.is_off():
+            self.toggle()
+        
+        self.do_set_brightness()
+    
+
+    def do_set_color(self) -> None:
+        """
+        Randomly set the color of the light.
+        """
+        self._set_light_attr(self.y_color)
+
+    
+    def set_color(self) -> None:
+        """
+        Template method to randomly set the color of the light.
+        The child class must implement the concrete method `is_on`,
+        which checks if the light is on.
+        """
+        # If light is off, turn it on
+        if self.is_off():
+            self.toggle()
+        
+        self.do_set_color()
diff --git a/smart_home_testbed/DeviceControl/PlugControl.py b/smart_home_testbed/DeviceControl/PlugControl.py
new file mode 100644
index 0000000..f4e2462
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/PlugControl.py
@@ -0,0 +1,14 @@
+from .DeviceControl import DeviceControl
+
+
+class PlugControl(DeviceControl):
+    """
+    Class to control a plug device.
+    """
+
+
+    def toggle(self) -> None:
+        """
+        Perform the toggle action on the plug.
+        """
+        self.get_phone().shell(f"input tap {self.x} {self.y}")
diff --git a/smart_home_testbed/DeviceControl/SmartThingsControl.py b/smart_home_testbed/DeviceControl/SmartThingsControl.py
new file mode 100644
index 0000000..5ea6419
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/SmartThingsControl.py
@@ -0,0 +1,34 @@
+import time
+from .DeviceControl import DeviceControl
+
+
+class SmartThingsControl(DeviceControl):
+    """
+    Abstract class to control a generic device through SmartThings.
+    """
+
+    ## Class variables
+    # Android package name
+    android_package = "com.samsung.android.oneconnect"
+    # Devices tab button screen coordinates
+    devices_x = 223.2
+    devices_y = 1281.6
+
+
+    def start_app(self) -> None:
+        """
+        Start the SmartThings app on the phone,
+        and open the device controls.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Start SmartThings app
+        phone.shell(f"monkey -p {SmartThingsControl.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open "Devices" tab
+        phone.shell(f"input tap {SmartThingsControl.devices_x} {SmartThingsControl.devices_y}")
+        time.sleep(5)
+        # Open device controls
+        phone.shell(f"input tap {self.device_x} {self.device_y}")
diff --git a/smart_home_testbed/DeviceControl/SmartThingsLightControl.py b/smart_home_testbed/DeviceControl/SmartThingsLightControl.py
new file mode 100644
index 0000000..f95d077
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/SmartThingsLightControl.py
@@ -0,0 +1,20 @@
+from .SmartThingsControl import SmartThingsControl
+from .LightControl import LightControl
+
+
+class SmartThingsLightControl(SmartThingsControl, LightControl):
+    """
+    Abstract class to control a generic light bulb through SmartThings.
+    """
+
+    ## Screen event coordinates
+    # Toggle
+    x = 626.4
+    y = 273.6
+    # Brightness and color gauges
+    x_left_gauge = 21
+    x_right_gauge = 692
+    # Brightness
+    y_brightness = 504
+    # Color
+    y_color = 1036.8
diff --git a/smart_home_testbed/DeviceControl/SmartThingsPlugControl.py b/smart_home_testbed/DeviceControl/SmartThingsPlugControl.py
new file mode 100644
index 0000000..2efe14a
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/SmartThingsPlugControl.py
@@ -0,0 +1,12 @@
+from .SmartThingsControl import SmartThingsControl
+from .PlugControl import PlugControl
+
+
+class SmartThingsPlugControl(SmartThingsControl, PlugControl):
+    """
+    Abstract class to control a generic plug through SmartThings.
+    """
+
+    # Toggle coordinates
+    x = 626.4
+    y = 273.6
diff --git a/smart_home_testbed/DeviceControl/TapoControl.py b/smart_home_testbed/DeviceControl/TapoControl.py
new file mode 100644
index 0000000..a216fab
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/TapoControl.py
@@ -0,0 +1,26 @@
+import time
+from .DeviceControl import DeviceControl
+
+
+class TapoControl(DeviceControl):
+    """
+    Abstract class to control a generic device through the Tapo app.
+    """
+
+    # Android package name
+    android_package = "com.tplink.iot"
+
+
+    def start_app(self) -> None:
+        """
+        Start the device's app on the phone.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Start Tapo app
+        phone.shell(f"monkey -p {TapoControl.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open device controls
+        phone.shell(f"input tap {self.device_x} {self.device_y}")
diff --git a/smart_home_testbed/DeviceControl/TapoPlugControl.py b/smart_home_testbed/DeviceControl/TapoPlugControl.py
new file mode 100644
index 0000000..06f29c2
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/TapoPlugControl.py
@@ -0,0 +1,12 @@
+from .TapoControl import TapoControl
+from .PlugControl import PlugControl
+
+
+class TapoPlugControl(TapoControl, PlugControl):
+    """
+    Abstract class to control a generic plug through the Tapo app.
+    """
+
+    # Toggle coordinates
+    x = 612
+    y = 619.2
diff --git a/smart_home_testbed/DeviceControl/TuyaControl.py b/smart_home_testbed/DeviceControl/TuyaControl.py
new file mode 100644
index 0000000..b9b7b6e
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/TuyaControl.py
@@ -0,0 +1,27 @@
+import time
+from .DeviceControl import DeviceControl
+
+
+class TuyaControl(DeviceControl):
+    """
+    Abstract class to control a generic device through the Tuya app.
+    """
+
+    # Metadata
+    android_package = "com.tuya.smart"
+    version = "3.3"
+
+
+    def start_app(self) -> None:
+        """
+        Start the Tuya app on the phone.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Start Tapo app
+        phone.shell(f"monkey -p {TuyaControl.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open device controls
+        phone.shell(f"input tap {self.device_x} {self.device_y}")
diff --git a/smart_home_testbed/DeviceControl/__init__.py b/smart_home_testbed/DeviceControl/__init__.py
new file mode 100644
index 0000000..6022ca2
--- /dev/null
+++ b/smart_home_testbed/DeviceControl/__init__.py
@@ -0,0 +1,10 @@
+from .DeviceControl import DeviceControl
+from .PlugControl import PlugControl
+from .LightControl import LightControl
+from .CameraControl import CameraControl
+from .TapoControl import TapoControl
+from .TapoPlugControl import TapoPlugControl
+from .SmartThingsControl import SmartThingsControl
+from .SmartThingsPlugControl import SmartThingsPlugControl
+from .SmartThingsLightControl import SmartThingsLightControl
+from .TuyaControl import TuyaControl
diff --git a/smart_home_testbed/DeviceState/CameraScreenshotState.py b/smart_home_testbed/DeviceState/CameraScreenshotState.py
new file mode 100644
index 0000000..be57395
--- /dev/null
+++ b/smart_home_testbed/DeviceState/CameraScreenshotState.py
@@ -0,0 +1,51 @@
+import cv2
+from skimage.metrics import structural_similarity as ssim
+from .CameraState import CameraState
+
+
+class CameraScreenshotState(CameraState):
+    """
+    Abstract class for the state of a camera device,
+    which uses screenshot-based event validation.
+    """
+
+    FILENAME_SCREENSHOT_STREAM = "stream.png"
+
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initialize the camera device with its IP address.
+
+        Args:
+            ipv4 (str): The device's IPv4 address.
+            kwargs (dict): device-specific additional parameters,
+                           including the path to the streaming screenshot.
+        """
+        # Call parent constructor
+        super().__init__(ipv4, **kwargs)
+
+        # Compute gray array of streaming screenshot
+        stream_image = cv2.imread(kwargs.get("path_screenshot_stream", CameraScreenshotState.FILENAME_SCREENSHOT_STREAM))
+        self.gray_stream_image = cv2.cvtColor(stream_image, cv2.COLOR_BGR2GRAY)
+
+
+    def get_state(self) -> bool:
+        """
+        Get the state of the camera device,
+        i.e. if it is currently streaming,
+        by taking a screenshot of its app.
+
+        Returns:
+            bool: True if the camera is currently streaming, False otherwise.
+        """
+        # Take screenshot and convert to grayscale
+        screenshot_array = self.screenshot()
+        gray_screenshot = cv2.cvtColor(screenshot_array, cv2.COLOR_BGR2GRAY)
+
+        # Compute SSIM
+        try:
+            score = ssim(self.gray_stream_image, gray_screenshot, full=False)
+            return score > self.SSIM_DIFF_THRESHOLD
+        except ValueError:
+            return False
diff --git a/smart_home_testbed/DeviceState/CameraState.py b/smart_home_testbed/DeviceState/CameraState.py
new file mode 100644
index 0000000..697caba
--- /dev/null
+++ b/smart_home_testbed/DeviceState/CameraState.py
@@ -0,0 +1,30 @@
+from .DeviceState import DeviceState
+
+
+class CameraState(DeviceState):
+    """
+    Abstract class for the state of a camera device.
+    """
+
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initialize the camera device with its IP address.
+
+        Args:
+            ipv4 (str): The device's IPv4 address.
+            kwargs (dict): device-specific additional parameters.
+        """
+        self.ipv4 = ipv4
+        self._was_last_stream_successful = False
+
+
+    def is_event_successful(self, _) -> bool:
+        """
+        Check if the last stream event was successful.
+
+        Returns:
+            bool: True if the event was successful, False otherwise.
+        """
+        return self._was_last_stream_successful
diff --git a/smart_home_testbed/DeviceState/DeviceState.py b/smart_home_testbed/DeviceState/DeviceState.py
new file mode 100644
index 0000000..2033ea5
--- /dev/null
+++ b/smart_home_testbed/DeviceState/DeviceState.py
@@ -0,0 +1,51 @@
+from typing import Any
+
+
+class DeviceState:
+    """
+    Abstract class which represent a device state.
+    """
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initialize the device with its IP address.
+
+        Args:
+            ipv4 (str): The device's IPv4 address.
+            kwargs (dict): device-specific additional parameters.
+        """
+        self.ipv4 = ipv4
+
+        # Set the device-specific parameters
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+
+    def get_state(self) -> Any:
+        """
+        Get the state of the device.
+
+        Returns:
+            Any: current state of the device.
+        """
+        raise NotImplementedError
+
+
+    def is_event_successful(self, previous_state: Any) -> bool:
+        """
+        Check if an event was successful,
+        i.e. if the device is still reachable
+        and its state has changed compared to the previous state.
+
+        Args:
+            previous_state (Any): The state of the device before the event.
+        Returns:
+            bool: True if the event was successful, False otherwise.
+        """
+        try:
+            state = self.get_state()
+        except Exception:
+            return False
+        else:
+            return state != previous_state
diff --git a/smart_home_testbed/DeviceState/HueState.py b/smart_home_testbed/DeviceState/HueState.py
new file mode 100644
index 0000000..ae51e68
--- /dev/null
+++ b/smart_home_testbed/DeviceState/HueState.py
@@ -0,0 +1,77 @@
+from typing import Awaitable
+from enum import Enum
+import contextlib
+import asyncio
+from aiohue import HueBridgeV2, create_app_key
+from .LightState import LightState
+
+
+class HueState(LightState):
+    """
+    Class to represent the state of a Philips Hue light bulb.
+    """
+
+
+    class StateKeys(Enum):
+        """
+        Enum for the keys in the dictionary representing the state of the Philips Hue device.
+        """
+        IS_ON      = "is_on"
+        BRIGHTNESS = "brightness"
+        COLOR_X    = "color_x"
+        COLOR_Y    = "color_y"
+
+    
+    @staticmethod
+    async def _get_app_key(bridge_ip: str) -> Awaitable[str]:
+        """
+        Asynchronously get the app key for the Philips Hue bridge.
+
+        Args:
+            bridge_ip (str): IP address of the Philips Hue bridge.
+        Returns:
+            Awaitable[str]: async function which returns the app key.
+        """
+        input("Press the link button on the bridge and press enter to continue...")
+        return await create_app_key(bridge_ip, "authentication_example")
+    
+
+    @classmethod
+    def get_app_key(cls, bridge_ip: str) -> str:
+        """
+        Get the app key for the Philips Hue bridge.
+
+        Args:
+            bridge_ip (str): IP address of the Philips Hue bridge.
+        Returns:
+            str: app key.
+        """
+        with contextlib.suppress(KeyboardInterrupt):
+            asyncio.run(cls._get_app_key(bridge_ip))
+
+    
+    async def _async_get_state(self) -> Awaitable[dict]:
+        """
+        Get the state of the Philips Hue light asynchronously.
+
+        Returns:
+            Awaitable[dict]: async function which returns the status of the Philips Hue light.
+        """
+        async with HueBridgeV2(self.ipv4, self.appkey) as bridge:
+            light = next(l for l in bridge.lights.items)
+            return {
+                self.StateKeys.IS_ON:      light.is_on,
+                self.StateKeys.BRIGHTNESS: light.brightness,
+                self.StateKeys.COLOR_X:    light.color.xy.x,
+                self.StateKeys.COLOR_Y:    light.color.xy.y
+            }
+        
+    
+    def get_state(self) -> dict:
+        """
+        Get the state of the Philips Hue light.
+
+        Returns:
+            dict: The status of the Philips Hue light.
+        """
+        return asyncio.run(self._async_get_state())
diff --git a/smart_home_testbed/DeviceState/LightState.py b/smart_home_testbed/DeviceState/LightState.py
new file mode 100644
index 0000000..dfe7f24
--- /dev/null
+++ b/smart_home_testbed/DeviceState/LightState.py
@@ -0,0 +1,27 @@
+from enum import Enum
+from .DeviceState import DeviceState
+
+
+class LightState(DeviceState):
+    """
+    Abstract class for the state of a light device.
+    """
+
+    class StateKeys(Enum):
+        """
+        Enum for the keys in the dictionary representing the state of a light device.
+        """
+        IS_ON      = "is_on"
+        BRIGHTNESS = "brightness"
+        HUE        = "hue"
+        SATURATION = "saturation"
+
+
+    def is_off(self) -> bool:
+        """
+        Check if the light is on.
+        
+        Returns:
+            bool: True if the light is on, False otherwise.
+        """
+        return not self.get_state()[self.StateKeys.IS_ON]
diff --git a/smart_home_testbed/DeviceState/ScreenshotState.py b/smart_home_testbed/DeviceState/ScreenshotState.py
new file mode 100644
index 0000000..4e6cf28
--- /dev/null
+++ b/smart_home_testbed/DeviceState/ScreenshotState.py
@@ -0,0 +1,46 @@
+import numpy as np
+import cv2
+from skimage.metrics import structural_similarity as ssim
+from .DeviceState import DeviceState
+
+
+class ScreenshotState(DeviceState):
+    """
+    Device state using app screenshot.
+    """
+
+    # SSIM threshold below which images are considered different
+    SSIM_DIFF_THRESHOLD = 0.95
+
+
+    def get_state(self) -> np.ndarray:
+        """
+        Get the state of the device,
+        by taking a screenshot of its app.
+
+        Returns:
+            numpy.ndarray: The screenshot as a numpy array.
+        """
+        return self.screenshot()
+    
+
+    def is_event_successful(self, previous_state: np.ndarray) -> bool:
+        """
+        Check if an event was successful,
+        i.e. if the current state has changed compared to the given previous state.
+        This method uses the SSIM metric to compare the screenshots.
+
+        Args:
+            previous_state (numpy.ndarray): app screenshot, as a numpy array, which represents the previous state.
+        Returns:
+            bool: True if the event was successful, False otherwise.
+        """
+        current_state = self.get_state()
+
+        # Load images, and convert to grayscale
+        gray_previous  = cv2.cvtColor(previous_state, cv2.COLOR_BGR2GRAY)
+        gray_current   = cv2.cvtColor(current_state, cv2.COLOR_BGR2GRAY)
+
+        # Compute SSIM
+        score = ssim(gray_previous, gray_current, full=False)
+        return score < self.SSIM_DIFF_THRESHOLD
diff --git a/smart_home_testbed/DeviceState/SmartThingsState.py b/smart_home_testbed/DeviceState/SmartThingsState.py
new file mode 100644
index 0000000..d97ee2c
--- /dev/null
+++ b/smart_home_testbed/DeviceState/SmartThingsState.py
@@ -0,0 +1,72 @@
+from typing import List, Any, Awaitable
+import asyncio
+import aiohttp
+from pysmartthings import SmartThings, DeviceEntity
+from .DeviceState import DeviceState
+
+
+class SmartThingsState(DeviceState):
+    """
+    Abstract class which represents a generic SmartThings device's state.
+    """
+
+
+    @staticmethod
+    async def _get_all_devices(token: str) -> Awaitable[List[DeviceEntity]]:
+        """
+        Asynchronously get all the devices connected to SmartThings.
+
+        Args:
+            token (str): SmartThings API token.
+        Returns:
+            Awaitable[List]: async function which returns the list of all devices.
+        """
+        async with aiohttp.ClientSession() as session:
+            api = SmartThings(session, token)
+            devices = await api.devices()
+            return devices
+        
+    
+    @classmethod
+    def get_all_devices(cls) -> List[DeviceEntity]:
+        """
+        Get all the devices connected to SmartThings.
+
+        Returns:
+            List: list of all devices
+        """
+        return asyncio.run(cls._get_all_devices())
+    
+
+    @classmethod
+    def print_all_devices(cls) -> None:
+        """
+        Print the label, name and id of all devices connected to SmartThings.
+        """
+        for device in cls.get_all_devices():
+            print(device.label, device.name, device.device_id)
+
+    
+    async def _async_get_state(self) -> Awaitable[Any]:
+        """
+        Asynchronously get the SmartThings device status.
+
+        Returns:
+            Awaitable[Any]: async function which returns the device status.
+        """
+        async with aiohttp.ClientSession() as session:
+            api = SmartThings(session, self.token)
+            devices = await api.devices(device_ids=[self.id])
+            device = devices[0]
+            await device.status.refresh()
+            return device.status
+    
+
+    def get_state(self) -> Any:
+        """
+        Get the device status.
+
+        Returns:
+            Any: device status
+        """
+        return asyncio.run(self._async_get_state())
diff --git a/smart_home_testbed/DeviceState/TapoLightState.py b/smart_home_testbed/DeviceState/TapoLightState.py
new file mode 100644
index 0000000..b512191
--- /dev/null
+++ b/smart_home_testbed/DeviceState/TapoLightState.py
@@ -0,0 +1,26 @@
+from typing import Awaitable
+from .TapoState import TapoState
+from .LightState import LightState
+
+
+class TapoLightState(TapoState, LightState):
+    """
+    Class to represent the state of a Tapo light bulb.
+    """
+
+
+    async def _async_get_state(self) -> Awaitable[dict]:
+        """
+        Get the state of the light asynchronously.
+
+        Returns:
+            Awaitable[dict]: async function which returns the state of the light.
+        """
+        dev = await self.client.l530(self.ipv4)
+        info = await dev.get_device_info()
+        return {
+            self.StateKeys.IS_ON:      info.device_on,
+            self.StateKeys.BRIGHTNESS: info.brightness,
+            self.StateKeys.HUE:        info.hue,
+            self.StateKeys.SATURATION: info.saturation
+        }
diff --git a/smart_home_testbed/DeviceState/TapoPlugState.py b/smart_home_testbed/DeviceState/TapoPlugState.py
new file mode 100644
index 0000000..2e75596
--- /dev/null
+++ b/smart_home_testbed/DeviceState/TapoPlugState.py
@@ -0,0 +1,19 @@
+from typing import Awaitable
+from .TapoState import TapoState
+
+
+class TapoPlugState(TapoState):
+    """
+    Class to represent the state of a Tapo plug.
+    """
+
+    async def _async_get_state(self) -> Awaitable[bool]:
+        """
+        Asynchronously get the state of the Tapo plug.
+
+        Returns:
+            Awaitable[bool]: async function which returns True if the plug is on, False otherwise.
+        """
+        dev = await self.client.p110(self.ipv4)
+        info = await dev.get_device_info()
+        return info.device_on
diff --git a/smart_home_testbed/DeviceState/TapoState.py b/smart_home_testbed/DeviceState/TapoState.py
new file mode 100644
index 0000000..08f44b8
--- /dev/null
+++ b/smart_home_testbed/DeviceState/TapoState.py
@@ -0,0 +1,50 @@
+from typing import Any, Awaitable
+import asyncio
+from tapo import ApiClient
+from .DeviceState import DeviceState
+
+
+class TapoState(DeviceState):
+    """
+    Class to represent the state of a generic Tapo device.
+    """
+
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initializes the Tapo device with its IP address,
+        and creates a Tapo API client.
+
+        Args:
+            ipv4 (str): The Tapo device's IPv4 address.
+            kwargs (dict): device-specific additional parameters,
+                           including user's credentials.
+        """
+        self.ipv4 = ipv4
+        self.client = ApiClient(
+            kwargs.get("username", ""),
+            kwargs.get("password", "")
+        )
+    
+
+    async def _async_get_state(self) -> Awaitable[Any]:
+        """
+        Asynchronously get the status of the generic Tapo device.
+
+        Returns:
+            Awaitable[Any]: async function which returns the Tapo device's status.
+        """
+        dev = await self.client.generic_device(self.ipv4)
+        info = await dev.get_device_info()
+        return info
+    
+
+    def get_state(self) -> Any:
+        """
+        Get the state of the Tapo device.
+
+        Returns:
+            Any: Tapo device status
+        """
+        return asyncio.run(self._async_get_state())
diff --git a/smart_home_testbed/DeviceState/TpLinkPlugState.py b/smart_home_testbed/DeviceState/TpLinkPlugState.py
new file mode 100644
index 0000000..ff07a05
--- /dev/null
+++ b/smart_home_testbed/DeviceState/TpLinkPlugState.py
@@ -0,0 +1,22 @@
+import asyncio
+from kasa import Discover
+from .DeviceState import DeviceState
+
+
+class TpLinkPlugState(DeviceState):
+    """
+    Class to represent the state of a TP-Link plug.
+    """
+
+
+    def get_state(self) -> bool:
+        """
+        Get the state of the plug.
+
+        Returns:
+            bool: True if the plug is on, False otherwise.
+        Raises:
+            TimeoutError: If the plug is not reachable.
+        """
+        dev = asyncio.run(Discover.discover_single(self.ipv4))
+        return dev.is_on
diff --git a/smart_home_testbed/DeviceState/__init__.py b/smart_home_testbed/DeviceState/__init__.py
new file mode 100644
index 0000000..1f3f6f6
--- /dev/null
+++ b/smart_home_testbed/DeviceState/__init__.py
@@ -0,0 +1,17 @@
+from .DeviceState import DeviceState
+from .ScreenshotState import ScreenshotState
+from .TapoState import TapoState
+from .SmartThingsState import SmartThingsState
+
+# Plugs
+from .TpLinkPlugState import TpLinkPlugState
+from .TapoPlugState import TapoPlugState
+
+# Lights
+from .LightState import LightState
+from .HueState import HueState
+from .TapoLightState import TapoLightState
+
+# Cameras
+from .CameraState import CameraState
+from .CameraScreenshotState import CameraScreenshotState
diff --git a/smart_home_testbed/__init__.py b/smart_home_testbed/__init__.py
new file mode 100644
index 0000000..6d400e9
--- /dev/null
+++ b/smart_home_testbed/__init__.py
@@ -0,0 +1,28 @@
+# Expose only concrete devices
+from .devices import (
+    init_device,
+
+    # Plugs
+    TpLinkPlug,
+    TpLinkPlugTapo,
+    TpLinkPlugSmartThings,
+    TapoPlug,
+    TapoPlugSmartThings,
+    SmartThingsOutlet,
+    TuyaPlug,
+    
+    # Lights
+    TapoLight,
+    TapoLightSmartThings,
+    HueLight,
+    HueLightEssentials,
+    HueLightSmartThings,
+    TuyaLight,
+
+    # Cameras
+    XiaomiCamera,
+    TapoCamera,
+    TapoCameraSmartThings,
+    DLinkCamera
+
+)
diff --git a/smart_home_testbed/devices/.gitignore b/smart_home_testbed/devices/.gitignore
new file mode 100644
index 0000000..b1542dd
--- /dev/null
+++ b/smart_home_testbed/devices/.gitignore
@@ -0,0 +1,8 @@
+# All experimental output and results
+*.png
+*.nft
+*.c
+*.txt
+*.pcap
+*.csv
+*.json
diff --git a/smart_home_testbed/devices/__init__.py b/smart_home_testbed/devices/__init__.py
new file mode 100644
index 0000000..cdb86ad
--- /dev/null
+++ b/smart_home_testbed/devices/__init__.py
@@ -0,0 +1,27 @@
+from .plugs import (
+    TpLinkPlug,
+    TpLinkPlugTapo,
+    TpLinkPlugSmartThings,
+    TapoPlug,
+    TapoPlugSmartThings,
+    SmartThingsOutlet,
+    TuyaPlug
+)
+
+from .lights import (
+    TapoLight,
+    TapoLightSmartThings,
+    HueLight,
+    HueLightEssentials,
+    HueLightSmartThings,
+    TuyaLight
+)
+
+from .cameras import (
+    XiaomiCamera,
+    TapoCamera,
+    TapoCameraSmartThings,
+    DLinkCamera
+)
+
+from .init_device import init_device
diff --git a/smart_home_testbed/devices/cameras/DLinkCamera/DLinkCamera.py b/smart_home_testbed/devices/cameras/DLinkCamera/DLinkCamera.py
new file mode 100644
index 0000000..3c2b9ac
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/DLinkCamera/DLinkCamera.py
@@ -0,0 +1,35 @@
+import os
+from ....DeviceState import CameraScreenshotState
+from ....DeviceControl import CameraControl
+
+
+class DLinkCamera(CameraScreenshotState, CameraControl):
+    """
+    DLink camera (DCS-8000LH).
+    """
+
+    ### Class attributes
+    # Android package
+    android_package = "com.dlink.mydlinkunified"
+    # SSIM threshold below which images are considered different
+    SSIM_DIFF_THRESHOLD = 0.9
+    ## Screen coordinates
+    # Stream event
+    x_start = 424.8
+    y_start = 907.2
+    x_stop = 57.6
+    y_stop = 1008
+    # Error message
+    x_error = 360
+    y_error = 806.4
+
+
+    def start_stream(self) -> None:
+        """
+        Dismiss the potential error message,
+        then start the camera's video stream.
+        Overwrites the method from the parent class CameraControl.
+        """
+        phone = self.get_phone()
+        phone.shell(f"input tap {self.x_error} {self.y_error}")
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
diff --git a/smart_home_testbed/devices/cameras/DLinkCamera/__init__.py b/smart_home_testbed/devices/cameras/DLinkCamera/__init__.py
new file mode 100644
index 0000000..6e9d82f
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/DLinkCamera/__init__.py
@@ -0,0 +1 @@
+from .DLinkCamera import DLinkCamera
diff --git a/smart_home_testbed/devices/cameras/TapoCamera/TapoCamera.py b/smart_home_testbed/devices/cameras/TapoCamera/TapoCamera.py
new file mode 100644
index 0000000..0d26b3e
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/TapoCamera/TapoCamera.py
@@ -0,0 +1,20 @@
+from ....DeviceState import CameraScreenshotState
+from ....DeviceControl import CameraControl
+
+
+class TapoCamera(CameraScreenshotState, CameraControl):
+    """
+    Tapo camera (C200).
+    """
+
+    ### Class attributes
+    # Android package
+    android_package = "com.tplink.iot"
+    # SSIM threshold below which images are considered different
+    SSIM_DIFF_THRESHOLD = 0.95
+    ## Screen coordinates
+    # Stream event
+    x_start = 187.2
+    y_start = 446.4
+    x_stop = 50.4
+    y_stop = 100.8
diff --git a/smart_home_testbed/devices/cameras/TapoCamera/__init__.py b/smart_home_testbed/devices/cameras/TapoCamera/__init__.py
new file mode 100644
index 0000000..79c1e41
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/TapoCamera/__init__.py
@@ -0,0 +1 @@
+from .TapoCamera import TapoCamera
diff --git a/smart_home_testbed/devices/cameras/TapoCameraSmartThings/TapoCameraSmartThings.py b/smart_home_testbed/devices/cameras/TapoCameraSmartThings/TapoCameraSmartThings.py
new file mode 100644
index 0000000..74f97db
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/TapoCameraSmartThings/TapoCameraSmartThings.py
@@ -0,0 +1,59 @@
+import time
+from ....DeviceState import CameraScreenshotState
+from ....DeviceControl import CameraControl, SmartThingsControl
+
+
+class TapoCameraSmartThings(CameraScreenshotState, CameraControl, SmartThingsControl):
+    """
+    Tapo camera (C200),
+    controlled through the SmartThings app.
+    """
+
+    ### Class attributes
+    # SSIM threshold below which images are considered different
+    SSIM_DIFF_THRESHOLD = 0.975
+    ## Screen coordinates
+    # Device controls button
+    device_x = 532.8
+    device_y = 792
+    # Expand stream
+    x_expand = 525.6
+    y_expand = 561.6
+    # Toggle stream
+    x_start = 720
+    y_start = 360
+
+
+    def start_app(self) -> None:
+        """
+        Start the SmartThings app on the phone,
+        and open the camera stream.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Start SmartThings app
+        phone.shell(f"monkey -p {SmartThingsControl.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open "Devices" tab
+        phone.shell(f"input tap {SmartThingsControl.devices_x} {SmartThingsControl.devices_y}")
+        time.sleep(5)
+        # Open device controls
+        phone.shell(f"input tap {self.device_x} {self.device_y}")
+        time.sleep(15)
+        # Expand stream to fullscreen
+        phone.shell(f"input tap {self.x_expand} {self.y_expand}")
+        phone.shell(f"input tap {self.x_expand} {self.y_expand}")
+        time.sleep(3)
+        # Pause stream
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
+    
+
+    def do_stop_stream(self) -> None:
+        """
+        Stop the camera's video stream.
+        """
+        phone = self.get_phone()
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
diff --git a/smart_home_testbed/devices/cameras/TapoCameraSmartThings/__init__.py b/smart_home_testbed/devices/cameras/TapoCameraSmartThings/__init__.py
new file mode 100644
index 0000000..b9ace01
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/TapoCameraSmartThings/__init__.py
@@ -0,0 +1 @@
+from .TapoCameraSmartThings import TapoCameraSmartThings
diff --git a/smart_home_testbed/devices/cameras/XiaomiCamera/XiaomiCamera.py b/smart_home_testbed/devices/cameras/XiaomiCamera/XiaomiCamera.py
new file mode 100644
index 0000000..4a8e641
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/XiaomiCamera/XiaomiCamera.py
@@ -0,0 +1,82 @@
+import time
+from miio import ChuangmiCamera
+from ....DeviceControl import CameraControl
+
+
+class XiaomiCamera(CameraControl, ChuangmiCamera):
+    """
+    Xiaomi camera (Chuangmi IPC019).
+    """
+
+    ### Class variables
+    # Android package name
+    android_package = "com.xiaomi.smarthome"
+    ## Screen coordinates
+    # Zone button
+    x_zone = 345.6
+    y_zone = 201.6
+    # Camera control button
+    x_camera = 619.2
+    y_camera = 331.2
+    # Stream event
+    x_start = 100.8
+    y_start = 489.6
+
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initialize the Xiaomi camera (Chuangmi IPC019) with its IP address.
+
+        Args:
+            ipv4 (str): The device's IPv4 address.
+            kwargs (dict): device-specific additional parameters, including device API token.
+        """
+        ChuangmiCamera.__init__(self, ip=ipv4, token=kwargs.get("token", ""))
+        self._was_last_stream_successful = False
+    
+
+    def start_app(self) -> None:
+        """
+        Start the Mi Home app on the phone,
+        then switch to the zone control screen,
+        then to the camera's stream.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        # Start the Mi Home app
+        phone = self.get_phone()
+        phone.shell(f"monkey -p {XiaomiCamera.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Switch to the camera control screen
+        phone.shell(f"input tap {self.x_zone} {self.y_zone}")
+        phone.shell(f"input tap {self.x_camera} {self.y_camera}")
+
+
+    def start_stream(self) -> None:
+        """
+        Start the camera's video stream.
+        Overrides the method from the `CameraControl` class.
+        """
+        phone = self.get_phone()
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
+        phone.shell(f"input tap {self.x_start} {self.y_start}")
+    
+
+    def do_stop_stream(self) -> None:
+        """
+        Stop the camera's video stream.
+        Overrides the method from the `CameraControl` class.
+        """
+        self.start_stream()
+    
+
+    def get_state(self) -> bool:
+        """
+        Get the state of the camera.
+
+        Returns:
+            bool: True if the camera is on, False otherwise.
+        """
+        return self.status().power
diff --git a/smart_home_testbed/devices/cameras/XiaomiCamera/__init__.py b/smart_home_testbed/devices/cameras/XiaomiCamera/__init__.py
new file mode 100644
index 0000000..23c032b
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/XiaomiCamera/__init__.py
@@ -0,0 +1 @@
+from .XiaomiCamera import XiaomiCamera
diff --git a/smart_home_testbed/devices/cameras/__init__.py b/smart_home_testbed/devices/cameras/__init__.py
new file mode 100644
index 0000000..6f48c9a
--- /dev/null
+++ b/smart_home_testbed/devices/cameras/__init__.py
@@ -0,0 +1,4 @@
+from .XiaomiCamera import XiaomiCamera
+from .TapoCamera import TapoCamera
+from .TapoCameraSmartThings import TapoCameraSmartThings
+from .DLinkCamera import DLinkCamera
diff --git a/smart_home_testbed/devices/init_device.py b/smart_home_testbed/devices/init_device.py
new file mode 100644
index 0000000..41508b6
--- /dev/null
+++ b/smart_home_testbed/devices/init_device.py
@@ -0,0 +1,20 @@
+from typing import Any
+import importlib
+
+
+def init_device(name: str, ipv4: str, **kwargs) -> Any:
+    """
+    Factory method to create a device object.
+
+    Args:
+        name (str): The name of the device.
+        ipv4 (str): The device's IPv4 address.
+        kwargs (dict): device-specific additional parameters.
+    Returns:
+        Any: The device object.
+    """
+    package_parts = importlib.import_module(__name__).__name__.split(".")
+    package_name = f"{package_parts[0]}.{package_parts[1]}"
+    package = importlib.import_module(package_name)
+    cls = getattr(package, name)
+    return cls(ipv4, **kwargs)
diff --git a/smart_home_testbed/devices/lights/HueLight/HueLight.py b/smart_home_testbed/devices/lights/HueLight/HueLight.py
new file mode 100644
index 0000000..6e9cf34
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLight/HueLight.py
@@ -0,0 +1,46 @@
+import time
+from ....DeviceState import HueState
+from ....DeviceControl import LightControl
+
+
+class HueLight(HueState, LightControl):
+    """
+    Philips Hue light bulb.
+    """
+
+    ### Class variables
+    # Android package
+    android_package = "com.philips.lighting.hue2"
+    ## Screen coordinates
+    # Zone button
+    x_zone = 360
+    y_zone = 446.4
+    # Toggle button
+    x = 136.8
+    y = 1022.4
+
+
+    def start_app(self) -> None:
+        """
+        Start the Philips Hue app on the phone,
+        and open the light control screen.
+
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Open app
+        phone.shell(f"monkey -p {self.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open zone controls
+        phone.shell(f"input tap {self.x_zone} {self.y_zone}")
+
+
+    def set_brightness(self) -> None:
+        raise NotImplementedError("Brightness cannot be set randomly for Philips Hue lights controlled through the Hue app.")
+    
+
+    def set_color(self) -> None:
+        raise NotImplementedError("Color cannot be set randomly for Philips Hue lights controlled through the Hue app.")
+
+
diff --git a/smart_home_testbed/devices/lights/HueLight/__init__.py b/smart_home_testbed/devices/lights/HueLight/__init__.py
new file mode 100644
index 0000000..41d22d2
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLight/__init__.py
@@ -0,0 +1 @@
+from .HueLight import HueLight
diff --git a/smart_home_testbed/devices/lights/HueLightEssentials/HueLightEssentials.py b/smart_home_testbed/devices/lights/HueLightEssentials/HueLightEssentials.py
new file mode 100644
index 0000000..266b13c
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLightEssentials/HueLightEssentials.py
@@ -0,0 +1,74 @@
+import time
+import math
+import random
+from ....DeviceState import HueState
+from ....DeviceControl import LightControl
+
+
+class HueLightEssentials(HueState, LightControl):
+    """
+    Philips Hue light bulb,
+    controlled by the Hue Essentials app.
+    """
+
+    ### Class variables
+    # Android package
+    android_package = "com.superthomaslab.hueessentials"
+    ## Screen coordinates
+    # Zone button
+    x_zone = 360
+    y_zone = 374.4
+    # Lamp button
+    x_lamp = 360
+    y_lamp = 475.2
+    # Toggle button
+    x = 626.4
+    y = 446.4
+    # Brightness
+    x_left_gauge = 67
+    x_right_gauge = 676
+    y_brightness = 230.4
+    # Color
+    x_color_center = 360
+    y_color_center = 792
+    y_color_top    = 472
+
+
+    def start_app(self, open_lamp_control: bool = False) -> None:
+        """
+        Start the Hue Essentials app on the phone,
+        and open the light control screen.
+
+        Args:
+            open_lamp_control (bool): Whether to additionally open the lamp control screen after having opened the app.
+                                      Optional, default is False.
+        Raises:
+            IndexError: If no adb device is found.
+        """
+        phone = self.get_phone()
+        # Open app
+        phone.shell(f"monkey -p {self.android_package} -c android.intent.category.LAUNCHER 1")
+        time.sleep(10)
+        # Open zone controls
+        phone.shell(f"input tap {self.x_zone} {self.y_zone}")
+        # If needed, open lamp controls
+        if open_lamp_control:
+            time.sleep(3)
+            phone.shell(f"input tap {self.x_lamp} {self.y_lamp}")
+    
+
+    def do_set_color(self) -> None:
+        """
+        Randomly set the color of the Philips Hue light
+        through the Hue Essentials app.
+        """
+        # Compute color wheel's radius
+        radius = self.y_color_center - self.y_color_top
+        # Generate random position on the color wheel
+        distance  = random.randint(0, radius)
+        angle_deg = random.randint(0, 360)
+        angle_rad = angle_deg * math.pi / 180
+        x = self.x_color_center + distance * math.cos(angle_rad)
+        y = self.y_color_center + distance * math.sin(angle_rad)
+        # Set the color
+        self.get_phone().shell(f"input tap {x} {y}")
diff --git a/smart_home_testbed/devices/lights/HueLightEssentials/__init__.py b/smart_home_testbed/devices/lights/HueLightEssentials/__init__.py
new file mode 100644
index 0000000..dd5761e
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLightEssentials/__init__.py
@@ -0,0 +1 @@
+from .HueLightEssentials import HueLightEssentials
diff --git a/smart_home_testbed/devices/lights/HueLightSmartThings/HueLightSmartThings.py b/smart_home_testbed/devices/lights/HueLightSmartThings/HueLightSmartThings.py
new file mode 100644
index 0000000..cc01707
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLightSmartThings/HueLightSmartThings.py
@@ -0,0 +1,14 @@
+from ....DeviceState import HueState
+from ....DeviceControl import SmartThingsLightControl
+
+
+class HueLightSmartThings(HueState, SmartThingsLightControl):
+    """
+    Philips Hue light bulb,
+    controlled by the SmartThings app.
+    """
+
+    ## Class variables
+    # Screen coordinates to open the device controls
+    device_x = 187.2
+    device_y = 1051.2
diff --git a/smart_home_testbed/devices/lights/HueLightSmartThings/__init__.py b/smart_home_testbed/devices/lights/HueLightSmartThings/__init__.py
new file mode 100644
index 0000000..e68bf0c
--- /dev/null
+++ b/smart_home_testbed/devices/lights/HueLightSmartThings/__init__.py
@@ -0,0 +1 @@
+from .HueLightSmartThings import HueLightSmartThings
diff --git a/smart_home_testbed/devices/lights/TapoLight/TapoLight.py b/smart_home_testbed/devices/lights/TapoLight/TapoLight.py
new file mode 100644
index 0000000..d8a2f24
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TapoLight/TapoLight.py
@@ -0,0 +1,34 @@
+from ....DeviceState import TapoLightState
+from ....DeviceControl import TapoControl, LightControl
+
+
+class TapoLight(TapoLightState, TapoControl, LightControl):
+    """
+    Tapo L530E light.
+    """
+
+    ## Class variables
+    # Device controls coordinates
+    device_x = 187.2
+    device_y = 705.6
+    # Toggle coordinates
+    x = 612
+    y = 619.2
+    # Brightness and color gauges
+    x_left_gauge = 103
+    x_right_gauge = 640
+    # Brightness
+    y_brightness = 1108.8
+    # Color
+    x_color_tab = 230.4
+    y_color_tab = 1238.4
+    y_color = 1310.4
+
+
+    def do_set_color(self) -> None:
+        """
+        Ensure the "Color" tab is selected,
+        then randomly set the color of the light. 
+        """
+        self.get_phone().shell(f"input tap {self.x_color_tab} {self.y_color_tab}")
+        self._set_light_attr(self.y_color)
diff --git a/smart_home_testbed/devices/lights/TapoLight/__init__.py b/smart_home_testbed/devices/lights/TapoLight/__init__.py
new file mode 100644
index 0000000..7eaedee
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TapoLight/__init__.py
@@ -0,0 +1 @@
+from .TapoLight import TapoLight
diff --git a/smart_home_testbed/devices/lights/TapoLightSmartThings/TapoLightSmartThings.py b/smart_home_testbed/devices/lights/TapoLightSmartThings/TapoLightSmartThings.py
new file mode 100644
index 0000000..4b04c7a
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TapoLightSmartThings/TapoLightSmartThings.py
@@ -0,0 +1,14 @@
+from ....DeviceState import TapoLightState
+from ....DeviceControl import SmartThingsLightControl
+
+
+class TapoLightSmartThings(TapoLightState, SmartThingsLightControl):
+    """
+    Tapo light bulb,
+    controlled by the SmartThings app.
+    """
+
+    ## Class variables
+    # Screen coordinates to open the device controls
+    device_x = 532.8
+    device_y = 1051.2
diff --git a/smart_home_testbed/devices/lights/TapoLightSmartThings/__init__.py b/smart_home_testbed/devices/lights/TapoLightSmartThings/__init__.py
new file mode 100644
index 0000000..b670d8d
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TapoLightSmartThings/__init__.py
@@ -0,0 +1 @@
+from .TapoLightSmartThings import TapoLightSmartThings
diff --git a/smart_home_testbed/devices/lights/TuyaLight/TuyaLight.py b/smart_home_testbed/devices/lights/TuyaLight/TuyaLight.py
new file mode 100644
index 0000000..56d3f12
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TuyaLight/TuyaLight.py
@@ -0,0 +1,84 @@
+import math
+import random
+import cv2
+from skimage.metrics import structural_similarity as ssim
+from ....DeviceState import ScreenshotState
+from ....DeviceControl import TuyaControl, LightControl
+
+
+class TuyaLight(ScreenshotState, TuyaControl, LightControl):
+    """
+    Generic Tuya light bulb.
+    """
+
+    # Default filename for the OFF screenshot
+    FILENAME_SCREENSHOT_OFF = "off.png"
+    # SSIM thresholds
+    SSIM_DIFF_THRESHOLD = 0.99      # State change
+    OFF_SSIM_DIFF_THRESHOLD = 0.95  # Check if light is off
+
+    ### Screen coordinates
+    # Device controls
+    device_x = 345.6
+    device_y = 590.4
+    ## Events
+    # Toggle
+    x = 360
+    y = 1281.6
+    # Brightness
+    x_left_gauge  = 164
+    x_right_gauge = 580
+    y_brightness  = 936
+    # Color
+    x_color_center = 360
+    y_color_center = 590.4
+    y_color_ring   = 400
+
+
+    def __init__(self, ipv4: str, **kwargs) -> None:
+        """
+        Constructor.
+        Initializes the Tuya light bulb with its IP address,
+        and computes the grayscale array of the bulb's OFF screenshot.
+
+        Args:
+            ipv4 (str): IP address of the Tuya light bulb.
+            kwargs (dict): device-specific additional parameters,
+                           including the path to the OFF screenshot.
+        """
+        # Call parent constructor
+        super().__init__(ipv4, **kwargs)
+
+        # Compute gray array of streaming screenshot
+        image_off = cv2.imread(kwargs.get("path_screenshot_off", TuyaLight.FILENAME_SCREENSHOT_OFF))
+        self.image_off_gray = cv2.cvtColor(image_off, cv2.COLOR_BGR2GRAY)
+    
+
+    def do_set_color(self) -> None:
+        """
+        Randomly set the color of the Tuya light.
+        """
+        # Generate random position on the color ring
+        radius = self.y_color_center - self.y_color_ring
+        angle_deg = random.randint(0, 360)
+        angle_rad = angle_deg * math.pi / 180
+        x = self.x_color_center + radius * math.cos(angle_rad)
+        y = self.y_color_center + radius * math.sin(angle_rad)
+        # Set the color
+        self.get_phone().shell(f"input tap {x} {y}")
+    
+
+    def is_off(self):
+        """
+        Check if the light is off.
+        """
+        # Take screenshot and convert to grayscale
+        state = self.get_state()
+        screenshot_gray = cv2.cvtColor(state, cv2.COLOR_BGR2GRAY)
+
+        # Compute SSIM with OFF screenshot
+        try:
+            score = ssim(self.image_off_gray, screenshot_gray, full=False)
+            return score > self.OFF_SSIM_DIFF_THRESHOLD
+        except ValueError:
+            return False
diff --git a/smart_home_testbed/devices/lights/TuyaLight/__init__.py b/smart_home_testbed/devices/lights/TuyaLight/__init__.py
new file mode 100644
index 0000000..9d1c1e4
--- /dev/null
+++ b/smart_home_testbed/devices/lights/TuyaLight/__init__.py
@@ -0,0 +1 @@
+from .TuyaLight import TuyaLight
diff --git a/smart_home_testbed/devices/lights/__init__.py b/smart_home_testbed/devices/lights/__init__.py
new file mode 100644
index 0000000..fc5a6d1
--- /dev/null
+++ b/smart_home_testbed/devices/lights/__init__.py
@@ -0,0 +1,6 @@
+from .TapoLight import TapoLight
+from .TapoLightSmartThings import TapoLightSmartThings
+from .HueLight import HueLight
+from .HueLightEssentials import HueLightEssentials
+from .HueLightSmartThings import HueLightSmartThings
+from .TuyaLight import TuyaLight
diff --git a/smart_home_testbed/devices/plugs/SmartThingsOutlet/SmartThingsOutlet.py b/smart_home_testbed/devices/plugs/SmartThingsOutlet/SmartThingsOutlet.py
new file mode 100644
index 0000000..fa217b6
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/SmartThingsOutlet/SmartThingsOutlet.py
@@ -0,0 +1,25 @@
+from typing import Awaitable
+from ....DeviceState import SmartThingsState
+from ....DeviceControl import SmartThingsPlugControl
+
+
+class SmartThingsOutlet(SmartThingsState, SmartThingsPlugControl):
+    """
+    SmartThings Outlet.
+    """
+
+    ## Class attributes
+    # Screen to open the device controls from the "Devices" tab
+    device_x = 187.2
+    device_y = 532.8
+
+
+    async def _async_get_state(self) -> Awaitable[bool]:
+        """
+        Asynchronously get the state of the plug.
+
+        Returns:
+            Awaitable[bool]: async function which returns True if the plug is on, False otherwise.
+        """
+        state = await super()._async_get_state()
+        return state.switch
diff --git a/smart_home_testbed/devices/plugs/SmartThingsOutlet/__init__.py b/smart_home_testbed/devices/plugs/SmartThingsOutlet/__init__.py
new file mode 100644
index 0000000..530bb4e
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/SmartThingsOutlet/__init__.py
@@ -0,0 +1 @@
+from .SmartThingsOutlet import SmartThingsOutlet
diff --git a/smart_home_testbed/devices/plugs/TapoPlug/TapoPlug.py b/smart_home_testbed/devices/plugs/TapoPlug/TapoPlug.py
new file mode 100644
index 0000000..4944bc5
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TapoPlug/TapoPlug.py
@@ -0,0 +1,12 @@
+from ....DeviceState import TapoPlugState
+from ....DeviceControl import TapoPlugControl
+
+
+class TapoPlug(TapoPlugState, TapoPlugControl):
+    """
+    Tapo P110 plug.
+    """
+    
+    # Screen coordinates for device controls
+    device_x = 532.8
+    device_y = 446.4
diff --git a/smart_home_testbed/devices/plugs/TapoPlug/__init__.py b/smart_home_testbed/devices/plugs/TapoPlug/__init__.py
new file mode 100644
index 0000000..2be0b89
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TapoPlug/__init__.py
@@ -0,0 +1 @@
+from .TapoPlug import TapoPlug
diff --git a/smart_home_testbed/devices/plugs/TapoPlugSmartThings/TapoPlugSmartThings.py b/smart_home_testbed/devices/plugs/TapoPlugSmartThings/TapoPlugSmartThings.py
new file mode 100644
index 0000000..da0a7ca
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TapoPlugSmartThings/TapoPlugSmartThings.py
@@ -0,0 +1,14 @@
+from ....DeviceState import TapoPlugState
+from ....DeviceControl import SmartThingsPlugControl
+
+
+class TapoPlugSmartThings(TapoPlugState, SmartThingsPlugControl):
+    """
+    Tapo P110 plug,
+    controlled by the SmartThings app.
+    """
+
+    ## Class attributes
+    # Screen coordinates to open the device controls
+    device_x = 187.2
+    device_y = 792
diff --git a/smart_home_testbed/devices/plugs/TapoPlugSmartThings/__init__.py b/smart_home_testbed/devices/plugs/TapoPlugSmartThings/__init__.py
new file mode 100644
index 0000000..158e84c
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TapoPlugSmartThings/__init__.py
@@ -0,0 +1 @@
+from .TapoPlugSmartThings import TapoPlugSmartThings
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlug/TpLinkPlug.py b/smart_home_testbed/devices/plugs/TpLinkPlug/TpLinkPlug.py
new file mode 100644
index 0000000..a91e915
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlug/TpLinkPlug.py
@@ -0,0 +1,15 @@
+from ....DeviceState import TpLinkPlugState
+from ....DeviceControl import PlugControl
+
+
+class TpLinkPlug(TpLinkPlugState, PlugControl):
+    """
+    TP-Link HS110 plug.
+    """
+    
+    ## Class variables
+    # Android package name
+    android_package = "com.tplink.kasa_android"
+    # Toggle coordinates
+    x = 620
+    y = 455
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlug/__init__.py b/smart_home_testbed/devices/plugs/TpLinkPlug/__init__.py
new file mode 100644
index 0000000..69691ea
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlug/__init__.py
@@ -0,0 +1 @@
+from .TpLinkPlug import TpLinkPlug
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/TpLinkPlugSmartThings.py b/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/TpLinkPlugSmartThings.py
new file mode 100644
index 0000000..3f5125c
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/TpLinkPlugSmartThings.py
@@ -0,0 +1,14 @@
+from ....DeviceState import TpLinkPlugState
+from ....DeviceControl import SmartThingsPlugControl
+
+
+class TpLinkPlugSmartThings(TpLinkPlugState, SmartThingsPlugControl):
+    """
+    TP-Link HS110 plug,
+    controlled through SmartThings.
+    """
+
+    ## Class attributes
+    # Screen to open the device controls
+    device_x = 532.8
+    device_y = 532.8
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/__init__.py b/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/__init__.py
new file mode 100644
index 0000000..92c0f0b
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlugSmartThings/__init__.py
@@ -0,0 +1 @@
+from .TpLinkPlugSmartThings import TpLinkPlugSmartThings
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlugTapo/TpLinkPlugTapo.py b/smart_home_testbed/devices/plugs/TpLinkPlugTapo/TpLinkPlugTapo.py
new file mode 100644
index 0000000..bb34228
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlugTapo/TpLinkPlugTapo.py
@@ -0,0 +1,13 @@
+from ....DeviceState import TpLinkPlugState
+from ....DeviceControl import TapoPlugControl
+
+
+class TpLinkPlugTapo(TpLinkPlugState, TapoPlugControl):
+    """
+    TP-Link HS110 plug,
+    controlled through the Tapo app.
+    """
+    
+    # Screen coordinates for device controls
+    device_x = 532.8
+    device_y = 705.6
diff --git a/smart_home_testbed/devices/plugs/TpLinkPlugTapo/__init__.py b/smart_home_testbed/devices/plugs/TpLinkPlugTapo/__init__.py
new file mode 100644
index 0000000..b3f343d
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TpLinkPlugTapo/__init__.py
@@ -0,0 +1 @@
+from .TpLinkPlugTapo import TpLinkPlugTapo
diff --git a/smart_home_testbed/devices/plugs/TuyaPlug/TuyaPlug.py b/smart_home_testbed/devices/plugs/TuyaPlug/TuyaPlug.py
new file mode 100644
index 0000000..9ba9de9
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TuyaPlug/TuyaPlug.py
@@ -0,0 +1,16 @@
+from ....DeviceState import ScreenshotState
+from ....DeviceControl import TuyaControl, PlugControl
+
+
+class TuyaPlug(ScreenshotState, TuyaControl, PlugControl):
+    """
+    Tuya generic plug.
+    """
+
+    ## Screen coordinates
+    # Device controls screen coordinates
+    device_x = 345.6
+    device_y = 360
+    # Toggle coordinates
+    x = 360
+    y = 561.6
diff --git a/smart_home_testbed/devices/plugs/TuyaPlug/__init__.py b/smart_home_testbed/devices/plugs/TuyaPlug/__init__.py
new file mode 100644
index 0000000..03ff64b
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/TuyaPlug/__init__.py
@@ -0,0 +1 @@
+from .TuyaPlug import TuyaPlug
diff --git a/smart_home_testbed/devices/plugs/__init__.py b/smart_home_testbed/devices/plugs/__init__.py
new file mode 100644
index 0000000..0f38dc4
--- /dev/null
+++ b/smart_home_testbed/devices/plugs/__init__.py
@@ -0,0 +1,7 @@
+from .TpLinkPlug import TpLinkPlug
+from .TpLinkPlugTapo import TpLinkPlugTapo
+from .TpLinkPlugSmartThings import TpLinkPlugSmartThings
+from .TapoPlug import TapoPlug
+from .TapoPlugSmartThings import TapoPlugSmartThings
+from .SmartThingsOutlet import SmartThingsOutlet
+from .TuyaPlug import TuyaPlug
-- 
GitLab